Self-Custody Use Case (ETH-like)
Данный гайд иллюстрирует создание секретов, синхронизацию адресов, получение баланса и отправку транзакции из клиентского приложения-кошелька в рамках сценария Self-Custody с использованием Spatium Signer Service.
Инициализация
Для взаимодействия со Spatium Signer Client необходимо предоставить следующие данные:
- auth - сессия авторизации (AuthorizationSession)
- storage - (StorageDirver) отвечающий за хранение клиентского секрета
import { MemoryStorageDriver, SpatiumCrypto } from '@spatium/sdk';
import { AuthorizationSession, ServiceStorage, SignerClient } from '@spatium/signer-client';
export const createSignerClient = (auth: AuthorizationSession) => {
const storage = new ServiceStorage('https://cloud.spatium.net/storage/v1', auth);
const cache = new MemoryStorageDriver()
const crypto = new SpatiumCrypto(cache, storage)
return new SignerClient('https://cloud.spatium.net/signer/v1', auth, crypto, 10 * 1000);
};
Важно! В данном примере в качестве хранилища используется ServiceStorage, который размещает оба секрета на стороне Spatium. Использование данного формата подразумевает, что кошелек является кастодиальным. Не рекомендуется к использованию в продакшене.
Генерация секрета
Для пользования распределенным кошельком необходимо создать постоянную пару из клиентского и серверного секретов и обеспечить их надежное хранение. На стороне Spatium Signer Service управление секретом происходит автоматически, а на стороне клиента разработчик должен имплементировать стабильный StorageDirver самостоятельно. В обоих случаях секрет привязывается к идентификатору секрета (secretId) и после создания доступен по нему. У одного пользователя может быть любое количество секретов, однако так как нужно обеспечить их сохранность и возможность восстановления, рекомендуется использовать один секрет на пользователя.
Для резервного копирования секретов на случай утраты содержимого StorageDirver рекомендуется использовать функционал экспорта и импорта.
export const ensureSecret = async (signerClient: SignerClient, secretId: string) => {
if (await signerClient.crypto.checkSecret(secretId)) {
return;
}
try {
// Ожидаем установки соедения
await signerClient.connect(10 * 1000);
await signerClient.generateDistributedSecret(secretId);
} finally {
await signerClient.disconnect();
}
};
Важно! На данном этапе разработки SDK рекомендуется использовать подобный подход при обращении к сервису, т.е. с подключением непосредственно перед взаимодействием и отключением после. Это позволит избежать распространенных сетевых ошибок пока они не будут полностью устранены.
Синхронизация адресов валют
Адрес валюты необходим для получения ассетов и запроса информации о балансе, поэтому рекомендуется сразу синхронизировать его при создании кошелька.
Для уменьшения времени синхронизации и улучшения пользовательского опыта рекомендуется использовать один публичный ключ (одни параметры синхронизации) для всех валют одной и той же криптосистемы. Для этого следует сначала синхронизировать один публичный ключ соответствующей криптосистемы, а затем сгенерировать с его помощью адреса в нужных валютах.
Каждая новая процедура синхронизации привязывается к уникальному идентификатору и ее результаты записываются в предоставленный StorageDirver, что дает возможность (имея сохраненный syncSessionId) синхронизировать ключ один раз и затем пользоваться результатами постоянно. Однако утрата данных синхронизации не несет долгосрочных последствий, так как ничего не мешает провести синхронизацию еще раз и получить при этом тот же самый публичный ключ и, соответственно, адреса.
Для процедуры синхронизации необходимы следующие данные:
- secretId - идентификатор секрета, служащего энтропией данного кошелька. На момент синхронизации секреты должны быть уже сгенерированы;
- syncSessionId - идентификатор сессии синхронизации. В случае совпадения предыдущая сессия с таким идентификатором будет перезаписана;
- curve - эллиптическая кривая. Для всех валют, поддерживаемых на текущий момент, это secp256k1;
- derivationCoin - параметр HD деривации ключа, непосредственно влияет на результат генерации адреса. Уникальные значения приводят к генерации уникальных ключей. Рекомендуется использовать фиксированное значение для конкретной криптосистемы, а варьировать значение ключа следующим параметром;
- derivationAccount - параметр HD деривации ключа, непосредственно влияет на результат генерации адреса. Уникальные значения приводят к генерации уникальных ключей.
import { syncDistributedEcdsaKey, getEcdsaPublicKey } from '@spatium/sdk';
export const ensureEcdsaPublicKey = async (signerClient: SignerClient, secretId: string, syncSessionId: string, derivationCoin: number, derivationAccount: number): Promise<string> => {
const publicKey = await getEcdsaPublicKey(signerClient, secretId, syncSessionId).catch(() => null);
if (publicKey) {
return publicKey;
}
try {
// Ожидаем установки соедения
await signerClient.connect(10 * 1000);
const distributedEcdsaKey = await syncDistributedEcdsaKey(signerClient, secretId, syncSessionId, 'secp256k1', derivationCoin, derivationAccount);
return distributedEcdsaKey
} finally {
await signerClient.disconnect();
}
};
Для получения из публичного ключа адреса в конкретном блокчейне рекомендуется использовать Blockchain Connector Service.
import axios from 'axios';
import { randomBytes, uuid } from '@spatium/sdk';
type EthLikeGetAddressRequest = {
network?: 'livenet' | 'testnet';
publicKey: string;
prefix?: boolean;
};
type EthLikeGetAddressResponse = {
requestId: string;
data: { address: string };
}
export const ethLikeGetAddress = async (
auth: AuthorizationSession,
chain: ETHLikeChain,
{ network, publicKey, prefix }: EthLikeGetAddressRequest,
): Promise<EthLikeGetAddressResponse['data']> => {
const token = await auth.getPermissionToken();
const response = await axios.post(
`https://cloud.spatium.net/blockchain-connector-eth-like/v1/api/get-address/${chain}`,
{
publicKey,
network,
prefix,
},
{
headers: {
'request-id': uuid(randomBytes),
'authorization': `Bearer ${token}`,
},
},
).then((result) => result.data);
return response.data;
};
Важно! Для того, чтобы при утрате данных в StorageDirver можно было однозначно восстановить все адреса со всеми средствами пользователя, нужно обеспечить бэкап клиентского секрета и внешнее хранение параметров генерации адресов, а именно:
- secretId - данные генерации, также нужно хранить вместе с бэкапом секрета
- curve - данные генерации
- derivationCoin - данные генерации
- derivationAccount - данные генерации
Получение информации об адресе
Имея синхронизированный адрес (или любой другой) можно обратиться к Address Info Service для получения развернутой информации об адресе, включая баланс в различных ассетах и историю транзакций.
import axios from 'axios';
import { randomBytes, uuid } from '@spatium/sdk';
type EthLikeGetAddressRequest = {
network?: 'livenet' | 'testnet';
address: string;
};
type EthLikeGetAddressResponse = {
requestId: string;
data: AddressInfo;
};
export const ethLikeGetAddressInfo = async (
auth: AuthorizationSession,
chain: ETHLikeChain,
{ network, address }: EthLikeGetAddressRequest,
): Promise<EthLikeGetAddressResponse['data']> => {
const token = await auth.getPermissionToken();
const result = await axios.get(
`https://cloud.spatium.net/address-info-eth-like/v1/api/${chain}`,
{
params: { network, address },
headers: {
'request-id': uuid(randomBytes),
'authorization': `Bearer ${token}`,
},
},
).then((result) => result.data);
return result.data;
};
Подпись транзакции
Подпись транзакции включает в себя несколько этапов:
- Сбор текущих данных из блокчейна, например, UTXO или nonce;
- Оценка комиссии транзакции;
- Формирование хэша транзакции;
- Подпись хэша транзакции;
- Формирование подписанной транзакции;
- Отправка транзакции в блокчейн.
Из всех этих этапов только подпись хэша транзакции выполняется при помощи SDK, остальное предоставляется через API Blockchain Connector Service.
Сбор nonce
Ethereum-подобные валюты для формирования транзакции требуют актуального на текущий момент nonce (одноразовый номер транзакции).
import axios from 'axios';
import { randomBytes, uuid } from '@spatium/sdk';
type EthLikeGetNonceRequest = {
network?: 'livenet' | 'testnet';
address: string;
};
type EthLikeGetNonceResponse = {
requestId: string;
data: { nonce: number };
};
export const ethLikeGetNonce = async (
auth: AuthorizationSession,
chain: ETHLikeChain,
{ network, address }: EthLikeGetNonceRequest
): Promise<EthLikeGetNonceResponse['data']> => {
const token = await auth.getPermissionToken();
const response = await axios.get(
`https://cloud.spatium.net/blockchain-connector-eth-like/v1/api/prepare-transaction/nonce/${chain}`,
{
params: { address, network },
headers: {
'request-id': uuid(randomBytes),
'authorization': `Bearer ${token}`,
},
},
).then((result) => result.data);
return response.data;
};
Сбор данных перевода ERC-20 токена
Важно! Данный метод используется только при отправке ERC-20 токенов, а при отправке нативной валюты блокчейна этот метод не используется и в параметр data последующих запросов для работы с транзакцией отправляется пустая строка.
Для отправки ERC-20 токенов необходимо получить соответствующие данные трансфера, эти данные передаются в поле data последующих запросов для работы с транзакцией.
import axios from 'axios';
import { randomBytes, uuid } from '@spatium/sdk';
type GetErc20TransferDataRequest = {
to: string;
amount: string;
};
type GetErc20TransferDataResponse = {
requestId: string;
data: { erc20TransferData: string };
};
export const getErc20TransferData = async (
auth: AuthorizationSession,
{ to, amount }: GetErc20TransferDataRequest
): Promise<GetErc20TransferDataResponse['data']> => {
const token = await auth.getPermissionToken();
const response = await axios.get(`https://cloud.spatium.net/blockchain-connector-eth-like/v1/api/prepare-transaction/erc20-transfer-data`, {
params: { to, amount },
headers: {
'request-id': uuid(randomBytes),
'authorization': `Bearer ${token}`,
},
}).then((result) => result.data);
return response.data;
};
Оценка объема работы для обработки транзакции
Для оценки размера комиссии в ethereum-подобных блокчейнах используется объем работы, выполняемый валидатором для обработки транзакции, для получения которого также нужно знать nonce.
import axios from 'axios';
import { randomBytes, uuid } from '@spatium/sdk';
type EthLikeGetGasLimitRequest = {
network?: 'livenet' | 'testnet';
to: string;
amount: string;
data: string;
nonce: number;
};
type EthLikeGetGasLimitResponse = {
requestId: string;
data: { gasLimit: string };
};
export const ethLikeGetGasLimit = async (
auth: AuthorizationSession,
chain: ETHLikeChain,
{ network, to, amount, data, nonce }: EthLikeGetGasLimitRequest
): Promise<EthLikeGetGasLimitResponse['data']> => {
const token = await auth.getPermissionToken();
const response = await axios.post(
`https://cloud.spatium.net/blockchain-connector-eth-like/v1/api/prepare-transaction/gas-limit/${chain}`,
{
network,
to,
amount,
data,
nonce,
},
{
params: { network },
headers: {
'request-id': uuid(randomBytes),
'authorization': `Bearer ${token}`,
},
},
).then((result) => result.data);
return response.data;
};
Получение информации о средней комиссии сети
import axios from 'axios';
import { randomBytes, uuid } from '@spatium/sdk';
type EthLikeGetGasPricesRequest = {
network?: 'livenet' | 'testnet';
};
type EthLikeGetGasPricesRequest = {
data: {[key: string] : {
date: number,
feeInfo: {
fast: string,
normal: string,
slow: string,
}
}}
}
export const ethLikeGetGasPrices = async (
auth: AuthorizationSession,
{ network }: EthLikeGetGasPricesRequest,
): Promise<EthLikeGetGasPricesRequest['data']> => {
const token = await auth.getPermissionToken();
const response = await axios.get(
`https://cloud.spatium.net/fee-info-eth-like/v1/static/fee-info-${network}.json`, {
params: { network },
headers: {
'request-id': uuid(randomBytes),
'authorization': `Bearer ${token}`,
},
},
).then((result) => result.data);
return response;
};
Расчет величины комиссии
Финальная величина оптимальной комиссии формируется из объема работы, выполняемой валидатором для обработки транзакции, и средней стоимости одной единицы работы.
Однако в рамках API расчет данной величины не требуется, так как параметры gasLimit и gasPrice для работы с транзакциями передаются как есть напрямую.
<!-- ### Общий запрос для оценки транзакции
Также, существует единый эндпоинт для получения оценки транзакции, в котором выполняется:
- Сбор nonce;
- Оценка объема работы для обработки транзакции;
- Получение информации о средней комиссии сети.
import axios from 'axios';
import { randomBytes, uuid } from '@spatium/sdk';
type EthLikeGetTxEstimateRequest = {
address: string;
network?: 'livenet' | 'testnet';
data: string;
to: string;
amount: string;
}
type EthLikeGetTxEstimateResponse = {
requestId: string;
data: {
nonce: number;
gasLimit: string;
gasPrices: { normal: string; fast: string; slow: string };
};
};
export const ethLikeGetTXEstimate = async (
auth: AuthorizationSession,
chain: ETHLikeChain,
{ address, network, data, to, amount }: EthLikeGetTxEstimateRequest,
): Promise<EthLikeGetTxEstimateResponse['data']> => {
const token = await auth.getPermissionToken();
const response = await axios.post(
`https://cloud.spatium.net/blockchain-connector-eth-like/v1/api/prepare-transaction/estimate/${chain}`,
{
address,
network,
data,
to,
amount,
},
{
headers: {
'request-id': uuid(randomBytes),
'authorization': `Bearer ${token}`,
},
},
).then((result) => result.data);
return response.data;
};
``` -->
### Формирование хэша для подписи
Имея все предварительные данные, можно формировать хэш для подписи.
```typescript
import axios from 'axios';
import { randomBytes, uuid } from '@spatium/sdk';
type EthLikeGetHashRequest = {
network?: 'livenet' | 'testnet';
publicKey: string;
to: string;
amount: string;
data: string;
nonce: number;
gasLimit: string;
gasPrice: string;
};
type EthLikeGetHashResponse = {
requestId: string;
data: { hash: string };
};
export const ethLikeGetHash = async (
auth: AuthorizationSession,
chain: ETHLikeChain,
{ network, publicKey, to, amount, data, nonce, gasLimit, gasPrice }: EthLikeGetHashRequest,
): Promise<EthLikeGetHashResponse['data']> => {
const token = await auth.getPermissionToken();
const response = await axios.post(
`https://cloud.spatium.net/blockchain-connector-eth-like/v1/api/transaction/get-hash/${chain}`,
{
network,
publicKey,
to,
amount,
data,
nonce,
gasLimit,
gasPrice,
},
{
headers: {
'request-id': uuid(randomBytes),
'authorization': `Bearer ${token}`,
},
},
).then((result) => result.data);
return response.data;
};
SMPC подпись хэша
import { signEcdsaMessage } from '@spatium/sdk';
import { randomBytes, uuid } from '@spatium/sdk';
export const signEcdsa = async (signerClient: SignerClient, secretId: string, syncSessionId: string, message: string): Promise<EcdsaSignature> => {
const signSessionId = uuid(randomBytes);
try {
// Ожидаем установки соедения
await signerClient.connect(10 * 1000);
return await signerClient.signEcdsaMessage(secretId, syncSessionId, signSessionId, message);
} finally {
await signerClient.disconnect();
}
};
Формирование подписанной транзакции
Подпись нужно прикрепить к транзакции, тем самым получив данные, готовые к отправке в блокчейн.
import axios from 'axios';
import { randomBytes, uuid } from '@spatium/sdk';
type EthLikeAttachSignatureRequest = {
network?: 'livenet' | 'testnet';
publicKey: string;
to: string;
amount: string;
data: string;
nonce: number;
gasLimit: string;
gasPrice: string;
signature: EcdsaSignature;
};
type EthLikeAttachSignatureResponse = {
requestId: string;
data: { txdata: string };
};
export const ethLikeAttachSignature = async (
auth: AuthorizationSession,
chain: ETHLikeChain,
{ network, publicKey, to, amount, data, nonce, gasLimit, gasPrice, signature }: EthLikeAttachSignatureRequest,
): Promise<EthLikeAttachSignatureResponse['data']> => {
const token = await auth.getPermissionToken();
const response = await axios.post(
`https://cloud.spatium.net/blockchain-connector-eth-like/v1/api/transaction/attach-signature/${chain}`,
{
network,
publicKey,
to,
amount,
data,
nonce,
gasLimit,
gasPrice,
signature,
},
{
headers: {
'request-id': uuid(randomBytes),
'authorization': `Bearer ${token}`,
},
},
).then((result) => result.data);
return response.data;
};
Отправка транзакции в сеть
За отправку транзакции в блокчейн также отвечает Blockchain Connector Service
import axios from 'axios';
import { randomBytes, uuid } from '@spatium/sdk';
type EthLikeSendTXRequest = {
network?: 'livenet' | 'testnet';
txdata: string;
};
type EthLikeSendTXResponse = {
requestId: string;
data: { txid: string };
};
export const ethLikeSendTX = async (
auth: AuthorizationSession,
chain: ETHLikeChain,
{ network, txdata }: EthLikeSendTXRequest,
): Promise<EthLikeSendTXResponse['data']> => {
const token = await auth.getPermissionToken();
const response = await axios.post(
`https://cloud.spatium.net/blockchain-connector-eth-like/v1/api/transaction/send/${chain}`,
{
network,
txdata,
},
{
headers: {
'request-id': uuid(randomBytes),
'authorization': `Bearer ${token}`,
},
}
).then((result) => result.data);
return response.data;
};
Полная процедура
import { AuthorizationSession, SignerClient } from '@spatium/signer-client';
import { randomBytes, uuid } from '@spatium/sdk';
const auth = new AuthorizationSession('https://cloud.spatium.net/authorization/v1', uuid(randomBytes), ['read', 'secret']);
// получение security токенов
const { data: { securityToken } } = await axios.post('https://cloud.spatium.net/authorization/v1/api/security-factor/credentials', {
username: 'username', password: 'password',
}, {
headers: {
'request-id': uuid(randomBytes),
},
}).then(({ data }) => data);
await auth.establish([securityToken]);
const etcLikeSignTransaction = async (auth: AuthorizationSession, signerClient: SignerClient, syncSessionId: string,
chain: ETHLikeChain, publicKey: string, to: string, amount: string) => {
const { nonce } = await ethLikeGetNonce(auth, 'eth', { address });
const { gasLimit } = await ethLikeGetGasLimit(auth, 'eth', { to, amount, data: '', nonce });
const pricePerByte = await getFee(auth, { network });
const gasPrices = pricePerByte.eth?.feeInfo
const { hash } = await ethLikeGetHash(auth, 'eth', { publicKey, to, amount, data: '', nonce, gasLimit, gasPrice: gasPrices.normal });
const signature = await signEcdsa(signerClient, syncSessionId, hash);
const { txdata } = await ethLikeAttachSignature(auth, 'eth', { publicKey, to, amount, data: '', nonce, gasLimit, gasPrice: gasPrices.normal, signature });
const { txid } = await ethLikeSendTX(auth, 'eth', { txdata });
return txid;
}