Self-Custody Use Case (ETH-like)
Данный гайд иллюстрирует создание секретов, синхронизацию адресов, получение баланса и отправку транзакции из клиентского приложения-кошелька в рамках сценария Self-Custody с использованием Spatium Signer Service.
Инициализация
Для взаимодействия со Spatium Signer Client необходимо предоставить следующие данные:
- auth - сессия авторизации (AuthorizationSession)
- storage - (StorageDirver) отвечающий за хранение клиентского секрета
import { MemoryStorageDriver, Crypto } 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 Crypto(cache, storage)
return new SignerClient('https://cloud.spatium.net/signer/v1', auth, crypto, 10 * 1000);
};
import SpatiumSDKSwift
import SignerClientSwift
func createSignerClient() -> SignerClient {
let auth = AuthorizationSession(
url: "https://cloud.spatium.net/authorization/v1",
tokenId: UUID().uuidString,
permissions: ["read", "secret"]
)
let clientCache = MemoryStorageDriver()
let clientStorage = MemoryStorageDriver()
let clientCrypto = SpatiumCrypto(cache: clientCache, storage: clientStorage)
let clientProtocol = SignerClient(
url: "https://cloud.spatium.net/signer/v1",
auth: auth,
crypto: clientCrypto,
timeout: 10 * 1000
)
return clientProtocol
}
Важно! В данном примере на TypeScript в качестве хранилища используется 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();
}
};
func ensureSecret(signerClient: SignerClient, secretId: String) async throws {
if (await signerClient.crypto.checkSecret(secretId: secretId)) {
return;
}
// Ожидаем установки соединения
try await signerClient.connect(timeout: 10 * 1000);
defer {
signerClient.disconnect()
}
try await signerClient.generateDistributedSecret(secretId: secretId);
};
Важно! На данном этапе разработки 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();
}
};
func ensureEcdsaPublicKey(signerClient: SignerClient, secretId: String, syncSessionId: String, derivationCoin: UInt32, derivationAccount: UInt32) async throws -> String {
let publicKey = try await getEcdsaPublicKey(driver: signerClient, secretId: secretId, syncSessionId: syncSessionId)
if !publicKey.isEmpty {
return publicKey
}
// Ожидаем установки соединения
try await signerClient.connect(timeout: 10 * 1000)
let distributedEcdsaKey = try await syncDistributedEcdsaKey(
driver: signerClient,
secretId: secretId,
syncSessionId: syncSessionId,
curve: .secp256k1,
derivationCoin: derivationCoin,
derivationAccount: derivationAccount
)
return distributedEcdsaKey
}
Для получения из публичного ключа адреса в конкретном блокчейне рекомендуется использовать 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;
};
enum ETHLikeChain: String, Codable {
case eth
case etc
case ftm
case avax
case matic
case bsc
case arb
case op
}
struct EthLikeGetAddressRequest: Codable {
enum Network: String, Codable {
case livenet
case testnet
}
let network: Network?
let publicKey: String
let prefix: Bool?
}
struct EthLikeGetAddressResponse: Decodable {
let requestId: String
let data: DataResponse
}
struct DataResponse: Decodable {
let address: String
}
func ethLikeGetAddress(auth: AuthorizationSession, chain: ETHLikeChain, addressRequest: EthLikeGetAddressRequest) async throws -> String? {
guard let url = URL(string: "https://cloud.spatium.net/blockchain-connector-eth-like/v1/api/get-address/\(chain)") else { throw CustomError.urlError }
let token = try await auth.getPermissionToken()
let headers: [String: String] = [
"request-id": UUID().uuidString,
"authorization": "Bearer \(token)"
]
var request = URLRequest(url: url)
request.httpMethod = "POST"
headers.forEach { (key, value) in
request.addValue(value, forHTTPHeaderField: key)
}
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
guard let body = try? JSONEncoder().encode(addressRequest) else {
throw CustomError.configurationError
}
request.httpBody = body
let (data,_) = try await URLSession.shared.data(for: request)
guard let address = try? JSONDecoder().decode(EthLikeGetAddressResponse.self, from: data) else {
throw CustomError.codingError
}
return address.data.address
}
Важно! Для того, чтобы при утрате данных в 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;
};
struct AddressInfo: Codable {
let address: String
let chain: String
let balances: [Balance]
// let transactions: [Transaction]
}
struct Balance: Codable {
let asset: Asset
let balance: BalanceAmount
}
struct Asset: Codable {
let chain: String
let kind: String
}
struct BalanceAmount: Codable {
let amount: String
let decimals: Int
}
// struct Transaction {
// // properties
// }
struct EthLikeGetAddressInfoRequest {
enum Network: String, Codable {
case livenet
case testnet
}
let network: Network?
let address: String
}
struct EthLikeGetAddressInfoResponse: Codable {
let requestId: String
let data: AddressInfo
}
func btcLikeGetAddressInfo(auth: AuthorizationSession,
chain: ETHLikeChain,
addressInfoRequest: EthLikeGetAddressInfoRequest) async throws -> AddressInfo {
let token = try await auth.getPermissionToken()
var url = URLComponents(string: "https://cloud.spatium.net/address-info-eth-like/v1/api/\(chain)")!
let params = [
"address": addressInfoRequest.address,
"network": addressInfoRequest.network?.rawValue
]
url.queryItems = params.map { (key, value) in
URLQueryItem(name: key, value: value)
}
var request = URLRequest(url: url.url!)
request.httpMethod = "GET"
request.addValue(UUID().uuidString, forHTTPHeaderField: "request-id")
request.addValue("Bearer \(token)", forHTTPHeaderField: "authorization")
let (data, _) = try await URLSession.shared.data(for: request)
let result = try JSONDecoder().decode(EthLikeGetAddressInfoResponse.self, from: 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;
};
struct EthLikeGetNonceRequest {
enum Network: String, Codable {
case livenet
case testnet
}
let network: Network?
let address: String
}
struct EthLikeGetNonceResponse: Codable {
var requestId: String
var data: NonceResponse
}
struct NonceResponse: Codable {
var nonce: Int
}
func ethLikeGetNonce(auth: AuthorizationSession, chain: ETHLikeChain, request: EthLikeGetNonceRequest) async throws -> Int {
let token = try await auth.getPermissionToken()
var url = URLComponents(string: "https://cloud.spatium.net/blockchain-connector-eth-like/v1/api/prepare-transaction/nonce/\(chain)")!
var params = [
"address": request.address
]
if let network = request.network {
params["network"] = network.rawValue
}
url.queryItems = params.map { (key, value) in
URLQueryItem(name: key, value: value)
}
let uuid = UUID().uuidString
let headers = [
"request-id": uuid,
"authorization": "Bearer \(token)"
]
var request = URLRequest(url: url.url!)
request.httpMethod = "GET"
headers.forEach { (key, value) in
request.addValue(value, forHTTPHeaderField: key)
}
let (data, _) = try await URLSession.shared.data(for: request)
let decodedResponse = try JSONDecoder().decode(EthLikeGetNonceResponse.self, from: data)
return decodedResponse.data.nonce
}
Сбор данных перевода 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;
};
struct GetErc20TransferDataRequest: Codable {
let to: String
let amount: String
}
struct GetErc20TransferDataResponse: Codable {
let requestId: String
let data: ERC20TransferData
}
struct ERC20TransferData: Codable {
let erc20TransferData: String
}
func getErc20TransferData(auth: AuthorizationSession, request: GetErc20TransferDataRequest) async throws -> ERC20TransferData {
let token = try await auth.getPermissionToken()
let url = URL(string: "https://cloud.spatium.net/blockchain-connector-eth-like/v1/api/prepare-transaction/erc20-transfer-data")!
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
urlRequest.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
urlRequest.addValue(UUID().uuidString, forHTTPHeaderField: "request-id")
guard let body = try? JSONEncoder().encode(request) else {
throw CustomError.configurationError
}
urlRequest.httpBody = body
let (data, _) = try await URLSession.shared.data(for: urlRequest)
let response = try JSONDecoder().decode(GetErc20TransferDataResponse.self, from: data)
return response.data
}
Сбор данных одобрения ERC-20 токена
Важно! Данный метод используется только при одобрении ERC-20 токенов, а при отправке нативной валюты блокчейна этот метод не используется и в параметр data последующих запросов для работы с транзакцией отправляется пустая строка.
Для одобрения ERC-20 токенов необходимо получить соответствующие данные одобрения, эти данные передаются в поле data последующих запросов для работы с транзакцией.
import axios from 'axios';
import { randomBytes, uuid } from '@spatium/sdk';
type GetErc20ApproveDataRequest = {
to: string;
amount: string;
};
type GetErc20ApproveDataResponse = {
requestId: string;
data: { erc20ApproveData: string };
};
export const getErc20ApproveData = async (
auth: AuthorizationSession,
{ to, amount }: GetErc20ApproveDataRequest
): Promise<GetErc20ApproveDataResponse['data']> => {
const token = await auth.getPermissionToken();
const response = await axios.get(`https://cloud.spatium.net/blockchain-connector-eth-like/v1/api/prepare-transaction/erc20-approve-data`, {
params: { to, amount },
headers: {
'request-id': uuid(randomBytes),
'authorization': `Bearer ${token}`,
},
}).then((result) => result.data);
return response.data;
};
struct GetErc20ApproveDataRequest {
let to: String
let amount: String
}
struct GetErc20ApproveDataResponse: Decodable {
let requestId: String
let data: Erc20ApproveData
}
struct Erc20ApproveData: Decodable {
let erc20ApproveData: String
}
func getErc20ApproveData(auth: AuthorizationSession,
request: GetErc20ApproveDataRequest) async throws -> Erc20ApproveData {
let token = try await auth.getPermissionToken()
var urlComponents = URLComponents(string: "https://cloud.spatium.net/blockchain-connector-eth-like/v1/api/prepare-transaction/erc20-approve-data")!
urlComponents.queryItems = [
URLQueryItem(name: "to", value: request.to),
URLQueryItem(name: "amount", value: request.amount)
]
var urlRequest = URLRequest(url: urlComponents.url!)
urlRequest.httpMethod = "GET"
urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
urlRequest.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
urlRequest.addValue(UUID().uuidString, forHTTPHeaderField: "request-id")
let (data, _) = try await URLSession.shared.data(for: urlRequest)
let response = try JSONDecoder().decode(GetErc20ApproveDataResponse.self, from: 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;
};
struct EthLikeGetGasLimitRequest: Encodable {
enum Network: String, Codable {
case livenet
case testnet
}
let network: Network?
let to: String
let amount: String
let data: String
let nonce: Int
}
struct EthLikeGetGasLimitResponse: Decodable {
let requestId: String
let data: GasLimit
}
struct GasLimit: Decodable {
let gasLimit: String
}
func ethLikeGetGasLimit(auth: AuthorizationSession,
chain: ETHLikeChain,
request: EthLikeGetGasLimitRequest) async throws -> GasLimit {
let token = try await auth.getPermissionToken()
let url = URL(string: "https://cloud.spatium.net/blockchain-connector-eth-like/v1/api/prepare-transaction/gas-limit/\(chain.rawValue)")!
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
urlRequest.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
urlRequest.addValue(UUID().uuidString, forHTTPHeaderField: "request-id")
let body = try JSONEncoder().encode(request)
urlRequest.httpBody = body
let (data, _) = try await URLSession.shared.data(for: urlRequest)
let response = try JSONDecoder().decode(EthLikeGetGasLimitResponse.self, from: 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;
};
enum Network: String, Codable {
case livenet
case testnet
}
struct GasPriceInfo: Decodable {
let date: Int
let feeInfo: FeeDetails
}
struct FeeDetails: Decodable {
let fast: String
let normal: String
let slow: String
}
func ethLikeGetGasPrices(auth: AuthorizationSession,
network: Network?) async throws -> [String: GasPriceInfo] {
let token = try await auth.getPermissionToken()
let url = URL(string: "https://cloud.spatium.net/fee-info-eth-like/v1/static/fee-info-\(network?.rawValue ?? "").json")!
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "GET"
urlRequest.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
urlRequest.addValue(UUID().uuidString, forHTTPHeaderField: "request-id")
let (data, _) = try await URLSession.shared.data(for: urlRequest)
let response = try JSONDecoder().decode([String: GasPriceInfo].self, from: data)
return response
}
Расчет величины комиссии
Финальная величина оптимальной комиссии формируется из объема работы, выполняемой валидатором для обработки транзакции, и средней стоимости одной единицы работы.
Однако в рамках API расчет данной величины не требуется, так как параметры gasLimit и gasPrice для работы с транзакциями передаются как есть напрямую.
Формирование хэша для подписи
Имея все предварительные данные, можно формировать хэш для подписи.
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;
};
struct EthLikeGetHashRequest: Encodable {
enum Network: String, Codable {
case livenet
case testnet
}
let network: Network?
let publicKey: String
let to: String
let amount: String
let data: String
let nonce: Int
let gasLimit: String
let gasPrice: String
}
struct EthLikeGetHashResponse: Decodable {
let requestId: String
let data: HashData
}
struct HashData: Decodable {
let hash: String
}
func ethLikeGetHash(auth: AuthorizationSession,
chain: ETHLikeChain,
request: EthLikeGetHashRequest) async throws -> HashData {
let token = try await auth.getPermissionToken()
let url = URL(string: "https://cloud.spatium.net/blockchain-connector-eth-like/v1/api/transaction/get-hash/\(chain.rawValue)")!
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
urlRequest.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
urlRequest.addValue(UUID().uuidString, forHTTPHeaderField: "request-id")
let body = try JSONEncoder().encode(request)
urlRequest.httpBody = body
let (data, _) = try await URLSession.shared.data(for: urlRequest)
let response = try JSONDecoder().decode(EthLikeGetHashResponse.self, from: data)
return response.data
}
Получение токена подписи
import { randomBytes, uuid } from '@spatium/sdk';
export const getSigningToken = async (message: string): Promise<string> => {
const { data: { permissionToken: { permissionToken: signingToken } } } = await axios.post('https://cloud.spatium.netauthorization/v1/api/permission/issue-by-key', {
tokenId: uuid(randomBytes),
permissions: ['sign'],
payload: message,
merchantKey: 'your-merchant-key',
}).then(({ data }) => data);
return signingToken;
};
// struct PermissionRefreshToken : Codable {
// let permissionRefreshToken : String?
// let issuer : String?
// let expiresAt : Int?
// }
struct PermissionToken : Codable {
let permissionToken : String
let issuer : String?
let expiresAt : Int?
}
struct TokenModel : Codable {
// let permissionRefreshToken : PermissionRefreshToken?
let permissionToken : PermissionToken
}
struct IssueKeyResponse : Codable {
let requestId : String
let data : TokenModel
}
func getSigningToken(message: String) async throws -> String {
let tokenId = UUID().uuidString
let url = URL(string: "https://cloud.spatium.net/authorization/v1/api/permission/issue-by-key")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue(UUID().uuidString, forHTTPHeaderField: "request-id")
let body: [String: Any] = [
"tokenId": tokenId,
"permissions": ["sign"],
"payload": message,
"merchantKey": Constants.merchantKey //"your-merchant-key"
]
request.httpBody = try JSONSerialization.data(withJSONObject: body, options: [])
let (data, _) = try await URLSession.shared.data(for: request)
guard let tokenModel = try? JSONDecoder().decode(IssueKeyResponse.self, from: data) else {
throw CustomError.codingError
}
return tokenModel.data.permissionToken.permissionToken
}
SMPC подпись хэша
import { signEcdsaMessage } from '@spatium/sdk';
import { randomBytes, uuid } from '@spatium/sdk';
export const signEcdsa = async (signerClient: SignerClient, secretId: string, syncSessionId: string, message: string, signingToken: string): Promise<EcdsaSignature> => {
const signSessionId = uuid(randomBytes);
try {
// Ожидаем установки соедения
await signerClient.connect(10 * 1000);
return await signerClient.signEcdsaMessage(secretId, syncSessionId, signSessionId, message, signingToken);
} finally {
await signerClient.disconnect();
}
};
func signEcdsa(signerClient: SignerClient, secretId: String, syncSessionId: String, message: String, signingToken: String) async throws -> EcdsaSignature {
let signSessionId = UUID().uuidString
// Wait for the actual connection to be established
try await signerClient.connect(timeout: 10 * 1000)
defer {
signerClient.disconnect()
}
do {
let signature = try await signerClient.signEcdsaMessage(secretId: secretId,
syncSessionId: syncSessionId,
signSessionId: signSessionId,
message: message,
signatureToken: signingToken)
return signature
} catch {
throw error
}
}
Формирование подписанной транзакции
Подпись нужно прикрепить к транзакции, тем самым получив данные, готовые к отправке в блокчейн.
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;
};
struct EthLikeAttachSignatureRequest: Encodable {
enum Network: String, Codable {
case livenet
case testnet
}
let network: Network?
let publicKey: String
let to: String
let amount: String
let data: String
let nonce: Int
let gasLimit: String
let gasPrice: String
let signature: EcdsaSignature
}
struct EthLikeAttachSignatureResponse: Decodable {
let requestId: String
let data: TxData
}
struct TxData: Decodable {
let txdata: String
}
func ethLikeAttachSignature(auth: AuthorizationSession,
chain: ETHLikeChain,
request: EthLikeAttachSignatureRequest) async throws -> TxData {
let token = try await auth.getPermissionToken()
let url = URL(string: "https://cloud.spatium.net/blockchain-connector-eth-like/v1/api/transaction/attach-signature/\(chain.rawValue)")!
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
urlRequest.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
urlRequest.addValue(UUID().uuidString, forHTTPHeaderField: "request-id")
let body = try JSONEncoder().encode(request)
urlRequest.httpBody = body
let (data, _) = try await URLSession.shared.data(for: urlRequest)
let response = try JSONDecoder().decode(EthLikeAttachSignatureResponse.self, from: 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;
};
struct EthLikeSendTXRequest: Encodable {
enum Network: String, Codable {
case livenet
case testnet
}
let network: Network?
let txdata: String
}
struct EthLikeSendTXResponse: Decodable {
let requestId: String
let data: TXID
}
struct TXID: Decodable {
let txid: String
}
func ethLikeSendTX(auth: AuthorizationSession,
chain: ETHLikeChain,
request: EthLikeSendTXRequest) async throws -> TXID {
let token = try await auth.getPermissionToken()
let url = URL(string: "https://cloud.spatium.net/blockchain-connector-eth-like/v1/api/transaction/send/\(chain.rawValue)")!
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
urlRequest.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
urlRequest.addValue(UUID().uuidString, forHTTPHeaderField: "request-id")
let body = try JSONEncoder().encode(request)
urlRequest.httpBody = body
let (data, _) = try await URLSession.shared.data(for: urlRequest)
print("ethLikeSendTX response: \(String(data:data, encoding: .utf8)!)")
let response = try JSONDecoder().decode(EthLikeSendTXResponse.self, from: 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 ethLikeSignTransaction = async (auth: AuthorizationSession, signerClient: SignerClient, syncSessionId: string,
chain: ETHLikeChain, publicKey: string, to: string, amount: string) => {
const { nonce } = await ethLikeGetNonce(auth, 'eth', { address });
// получение данных трансфера erc20
// const { erc20TransferData } = await erc20GetTransfer(auth, { to, amount });
// получение данных одобрения erc20
// const { erc20ApproveData } = await erc20GetApprove(auth, { to, amount });
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 signingToken = await getSigningToken(hash)
const signature = await signEcdsa(signerClient, syncSessionId, hash, signingToken);
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;
}
struct SecurityTokenResponseData : Codable, Equatable {
let securityToken: String
}
struct SecurityTokenResponse : Codable, Equatable {
let requestId: String
let data: SecurityTokenResponseData
}
let auth = AuthorizationSession(url: "https://cloud.spatium.net/authorization/v1", tokenId: UUID().uuidString, permissions: ["read", "secret"])
// get security tokens
let url = URL(string: "https://cloud.spatium.net/authorization/v1/api/security-factor/credentials")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
request.addValue(UUID().uuidString, forHTTPHeaderField: "request-id")
guard let body = try? JSONEncoder().encode(["username": "username", "password": "password"]) else {
throw CustomError.configurationError
}
request.httpBody = body
let (data, _) = try await URLSession.shared.data(for: request)
let response = try JSONDecoder().decode(SecurityTokenResponse.self, from: data)
try await auth.establish([response.data.securityToken])
func ethLikeSignTransaction(
auth: AuthorizationSession,
secretId: String,
signerClient: SignerClient,
syncSessionId: String,
chain: ETHLikeChain,
publicKey: String,
to: String,
amount: String) async throws -> String {
guard let address = try await ethLikeGetAddress(
auth: auth,
chain: .eth,
addressRequest: EthLikeGetAddressRequest(
network: .testnet,
publicKey: publicKey,
prefix: false
)
) else {
throw CustomError.configurationError
}
let nonce = try await ethLikeGetNonce(
auth: auth,
chain: .eth,
request: EthLikeGetNonceRequest(
network: .testnet,
address: address
)
)
let gasLimitResult = try await ethLikeGetGasLimit(
auth: auth,
chain: .eth,
request: EthLikeGetGasLimitRequest(
network: .testnet,
to: to,
amount: amount,
data: "",
nonce: nonce
)
)
let gasLimit = gasLimitResult.gasLimit
let gasPricesResult = try await ethLikeGetGasPrices(auth: auth, network: .testnet)
let gasPrice = gasPricesResult["eth"]?.feeInfo.normal ?? "0"
let hashResult = try await ethLikeGetHash(
auth: auth,
chain: .eth,
request: EthLikeGetHashRequest(
network: .testnet,
publicKey: publicKey,
to: to,
amount: amount,
data: "",
nonce: nonce,
gasLimit: gasLimit,
gasPrice: gasPrice
)
)
let hash = hashResult.hash
let signingToken = try await getSigningToken(message: hash)
let signature = try await signEcdsa(
signerClient: signerClient,
secretId: secretId,
syncSessionId: syncSessionId,
message: hash,
signingToken: signingToken
)
let txResult = try await ethLikeAttachSignature(
auth: auth,
chain: .eth,
request: EthLikeAttachSignatureRequest(
network: .testnet,
publicKey: publicKey,
to: to,
amount: amount,
data: "",
nonce: nonce,
gasLimit: gasLimit,
gasPrice: gasPrice,
signature: signature
)
)
let txdata = txResult.txdata
let txidResult = try await ethLikeSendTX(
auth: auth,
chain: .eth,
request: EthLikeSendTXRequest(
network: .testnet,
txdata: txdata
)
)
let txid = txidResult.txid
return txid
}