Self-Custody use case (BTC-like)
Данный гайд иллюстрирует создание секретов, синхронизацию адресов, получение баланса и отправку транзакции из клиентского приложения-кошелька в рамках сценария Self-Custody с использованием Spatium Signer Service.
Инициализация
Для взаимодействия со Spatium Signer Client необходимо предоставить следующие данные:
- auth - сессия авторизации (AuthorizationSession)
- crypto - любой валидный Crypto для обработки данных
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 BtcLikeGetAddressRequest = {
network?: 'livenet' | 'testnet';
publicKey: string;
type?: 'p2pkh' | 'p2wpkh';
prefix?: boolean;
};
type BtcLikeGetAddressResponse = {
requestId: string;
data: { address: string };
};
export const btcLikeGetAddress = async (
auth: AuthorizationSession,
chain: BTCLikeChain,
{ network, publicKey, type, prefix }: BtcLikeGetAddressRequest,
): Promise<BtcLikeGetAddressResponse['data']> => {
const token = await auth.getPermissionToken();
const response = await axios.post(
`https://cloud.spatium.net/blockchain-connector-btc-like/v1/api/get-address/${chain}`,
{
publicKey,
network,
type,
prefix,
},
{
headers: {
'request-id': uuid(randomBytes),
'authorization': `Bearer ${token}`,
},
},
).then((result) => result.data);
return response.data;
};
enum BTCLikeChain: String, Codable {
case btc
case ltc
case bch
case doge
}
struct BtcLikeGetAddressRequest: Codable {
enum Network: String, Codable {
case livenet
case testnet
}
enum AddressType: String, Codable {
case p2pkh
case p2wpkh
}
let network: Network?
let publicKey: String
let type: AddressType
let prefix: Bool?
}
struct BtcLikeGetAddressResponse: Decodable {
let requestId: String
let data: DataResponse
}
struct DataResponse: Decodable {
let address: String
}
func btcLikeGetAddress(auth: AuthorizationSession, chain: BTCLikeChain, addressRequest: BtcLikeGetAddressRequest) async throws -> String? {
guard let url = URL(string: "https://cloud.spatium.net/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(BtcLikeGetAddressResponse.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 AddressInfo = {
address: string;
chain: string;
balances: [
{
asset: { chain: string, kind: string },
balance: { amount: string, decimals: number }
}
];
transactions: [];
}
type BtcLikeGetAddressInfoRequest = {
network?: 'livenet' | 'testnet';
address: string;
};
type BtcLikeGetAddressInfoResponse = {
requestId: string;
data: AddressInfo;
};
export const btcLikeGetAddressInfo = async (
auth: AuthorizationSession,
chain: BTCLikeChain,
{ network, address }: BtcLikeGetAddressInfoRequest,
): Promise<BtcLikeGetAddressInfoResponse['data']> => {
const token = await auth.getPermissionToken();
const result = await axios.get(
`https://cloud.spatium.net/address-info-btc-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 BtcLikeGetAddressInfoRequest {
enum Network: String, Codable {
case livenet
case testnet
}
let network: Network?
let address: String
}
struct BtcLikeGetAddressInfoResponse: Codable {
let requestId: String
let data: AddressInfo
}
func btcLikeGetAddressInfo(auth: AuthorizationSession,
chain: BTCLikeChain,
addressInfoRequest: BtcLikeGetAddressInfoRequest) async throws -> AddressInfo {
let token = try await auth.getPermissionToken()
var url = URLComponents(string: "https://cloud.spatium.net/address-info-btc-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(BtcLikeGetAddressInfoResponse.self, from: data)
return result.data
}
Подпись транзакции
Подпись транзакции включает в себя несколько этапов:
- Сбор текущих данных из блокчейна, например, UTXO или nonce;
- Оценка комиссии транзакции;
- Формирование хэша транзакции;
- Подпись хэша транзакции;
- Формирование подписанной транзакции;
- Отправка транзакции в блокчейн.
Из всех этих этапов только подпись хэша транзакции выполняется при помощи SDK, остальное предоставляется через API Blockchain Connector Service.
Сбор UTXO
Биткоин-подобные валюты для формирования транзакции требуют актуальных на текущий момент UTXO (Unspent Transaction Outputs).
import axios from 'axios';
import { randomBytes, uuid } from '@spatium/sdk';
type BtcLikeGetUTXORequest = {
network?: 'livenet' | 'testnet';
address: string;
};
type UTXO = {
txid: string;
vout: number;
value: string;
};
type BtcLikeGetUTXOResponse = {
requestId: string;
data: { utxo: UTXO[] };
};
export const btcLikeGetUTXO = async (
auth: AuthorizationSession,
chain: BTCLikeChain,
{ network, address }: BtcLikeGetUTXORequest,
): Promise<BtcLikeGetUTXOResponse['data']> => {
const token = await auth.getPermissionToken();
const response = await axios.get(
`https://cloud.spatium.net/blockchain-connector-btc-like/v1/api/prepare-transaction/utxo-list/${chain}`,
{
params: { network, address },
headers: {
'request-id': uuid(randomBytes),
'authorization': `Bearer ${token}`,
},
},
).then((result) => result.data);
return response.data;
};
struct BtcLikeGetUTXORequest {
enum Network: String, Codable {
case livenet
case testnet
}
let network: Network?
let address: String
}
struct UTXO: Codable {
let txid: String
let vout: Int
let value: String
}
struct UTXData: Codable {
let utxo: [UTXO]
}
struct BtcLikeGetUTXOResponse: Codable {
let requestId: String
let data: UTXData
}
func btcLikeGetUTXO(auth: AuthorizationSession, chain: BTCLikeChain, request: BtcLikeGetUTXORequest) async throws -> [UTXO] {
let token = try await auth.getPermissionToken()
var url = URLComponents(string:"https://cloud.spatium.net/blockchain-connector-btc-like/v1/api/prepare-transaction/utxo-list/\(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(BtcLikeGetUTXOResponse.self, from: data)
return decodedResponse.data.utxo
}
Оценка размера транзакции
Для оценки размера комиссии в биткоин-подобных блокчейнах используется размер транзакции в байтах, для получения которого также нужно знать UTXO.
import axios from 'axios';
import { randomBytes, uuid } from '@spatium/sdk';
type BtcLikeGetTXSizeRequest = {
network?: 'livenet' | 'testnet';
publicKey: string;
type?: 'p2pkh' | 'p2wpkh';
utxo: { txid: string; vout: number; value: string }[];
to: string;
amount: string;
};
type BtcLikeGetTXSizeResponse = {
requestId: string;
data: { size: number };
};
export const btcLikeGetTXSize = async (
auth: AuthorizationSession,
chain: BTCLikeChain,
{ network, publicKey, type, utxo, to, amount }: BtcLikeGetTXSizeRequest,
): Promise<BtcLikeGetTXSizeResponse['data']> => {
const token = await auth.getPermissionToken();
const response = await axios.post(
`https://cloud.spatium.net/blockchain-connector-btc-like/v1/api/prepare-transaction/size/${chain}`,
{
publicKey,
network,
type,
utxo,
to,
amount,
},
{
headers: {
'request-id': uuid(randomBytes),
'authorization': `Bearer ${token}`,
},
},
).then((result) => result.data);
return response.data;
};
struct BtcLikeGetTXSizeRequest: Codable {
enum Network: String, Codable {
case livenet
case testnet
}
enum AddressType: String, Codable {
case p2pkh
case p2wpkh
}
let network: Network?
let publicKey: String
let type: AddressType?
let utxo: [UTXO]
let to: String
let amount: String
}
struct UTXO: Codable {
let txid: String
let vout: Int
let value: String
}
struct BtcLikeGetTXSizeResponse: Codable {
let requestId: String
let data: SizeResponse
}
struct SizeResponse: Codable {
let size: Int
}
func btcLikeGetTXSize(auth: AuthorizationSession, chain: BTCLikeChain, sizeRequest: BtcLikeGetTXSizeRequest) async throws -> SizeResponse {
let token = try await auth.getPermissionToken()
guard let url = URL(string: "https://cloud.spatium.net/blockchain-connector-btc-like/v1/api/prepare-transaction/size/\(chain)")
else { throw CustomError.urlError}
let headers = [
"request-id": UUID().uuidString,
"authorization": "Bearer \(token)",
"Content-Type": "application/json"
]
var request = URLRequest(url: url)
request.httpMethod = "POST"
headers.forEach { (key, value) in
request.addValue(value, forHTTPHeaderField: key)
}
guard let body = try? JSONEncoder().encode(sizeRequest) else {
throw CustomError.configurationError
}
request.httpBody = body
let (data, _) = try await URLSession.shared.data(for: request)
let result = try JSONDecoder().decode(BtcLikeGetTXSizeResponse.self, from: data)
return result.data
}
Получение информации о средней комиссии сети
import axios from 'axios';
import { randomBytes, uuid } from '@spatium/sdk';
type BtcLikeGetBytePricesRequest = {
network?: 'livenet' | 'testnet';
};
type BtcLikeGetBytePricesResponse = {
data: {[key: string] : {
date: number,
feeInfo: {
fast: string,
normal: string,
slow: string,
}
}}
}
export const btcLikeGetBytePrices = async (
auth: AuthorizationSession,
{ network }: BtcLikeGetBytePricesRequest,
): Promise<BtcLikeGetBytePricesResponse['data']> => {
const token = await auth.getPermissionToken();
const response = await axios.get(
`https://cloud.spatium.net/fee-info-btc-like/v1/static/fee-info-${network}.json`, {
params: { network },
headers: {
'request-id': uuid(randomBytes),
'authorization': `Bearer ${token}`,
},
},
).then((result) => result.data);
return response;
};
struct BtcLikeGetBytePricesRequest: Codable {
enum Network: String, Codable {
case livenet
case testnet
}
let network: Network?
}
struct BtcLikeGetBytePricesResponse: Codable {
let data: [String: BtcLikeFeeInfo]
}
struct BtcLikeFeeInfo: Codable {
let date: Date
let feeInfo: BtcLikeFeeDetails
}
struct BtcLikeFeeDetails: Codable {
let fast: String
let normal: String
let slow: String
}
func btcLikeGetBytePrices(auth: AuthorizationSession, request: BtcLikeGetBytePricesRequest) async throws -> [String: BtcLikeFeeInfo] {
let token = try await auth.getPermissionToken()
let url = URL(string: "https://cloud.spatium.net/fee-info-btc-like/v1/static/fee-info-\(request.network?.rawValue ?? "livenet").json")!
var request = URLRequest(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 response = try JSONDecoder().decode([String: BtcLikeFeeInfo].self, from: data)
return response
}
Расчет величины комиссии
Финальная величина оптимальной комиссии формируется из размера транзакции и средней стоимости одного байта. Также можно ввести общую сумму комиссии вручную, игнорируя и размер, и оценку.
Формирование хэша для подписи
Имея все предварительные данные, можно формировать хэш для подписи (в данном случае - набор хэшей).
import axios from 'axios';
import { randomBytes, uuid } from '@spatium/sdk';
type BtcLikeGetHashRequest = {
network?: 'livenet' | 'testnet';
publicKey: string;
type?: 'p2pkh' | 'p2wpkh';
utxo: { txid: string; vout: number; value: string }[];
to: string;
amount: string;
fee: string;
};
type BtcLikeGetHashResponse = {
requestId: string;
data: {
inputIndex: number;
hash: string;
}[];
};
export const btcLikeGetHash = async (
auth: AuthorizationSession,
chain: BTCLikeChain,
{ network, publicKey, type, utxo, to, amount, fee }: BtcLikeGetHashRequest,
): Promise<BtcLikeGetHashResponse['data']> => {
const token = await auth.getPermissionToken();
const response = await axios.post(
`https://cloud.spatium.net/blockchain-connector-btc-like/v1/api/transaction/get-hash/${chain}`,
{
network,
publicKey,
type,
utxo,
to,
amount,
fee,
},
{
headers: {
'request-id': uuid(randomBytes),
'authorization': `Bearer ${token}`,
},
},
).then((result) => result.data);
return response.data;
};
struct BtcLikeGetHashRequest: Codable {
var network: Network?
var publicKey: String
var type: AddressType?
var utxo: [UTXO]
var to: String
var amount: String
var fee: String
}
struct UTXO: Codable {
var txid: String
var vout: Int
var value: String
}
struct BtcLikeGetHashResponse: Codable {
var requestId: String
var data: DataHashes
}
struct DataHashes : Codable {
let hashes : [HashData]?
}
struct HashData: Codable {
var inputIndex: Int
var hash: String
}
func btcLikeGetHash(auth: AuthorizationSession, chain: BTCLikeChain, request: BtcLikeGetHashRequest) async throws -> [HashData]? {
let token = try await auth.getPermissionToken()
let url = "https://cloud.spatium.net/blockchain-connector-btc-like/v1/api/transaction/get-hash/\(chain)"
var urlRequest = URLRequest(url: URL(string: url)!)
urlRequest.httpMethod = "POST"
urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
urlRequest.addValue(UUID().uuidString, forHTTPHeaderField: "request-id")
urlRequest.addValue("Bearer \(token)", forHTTPHeaderField: "authorization")
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(BtcLikeGetHashResponse.self, from: data)
return response.data.hashes
}
Получение токена подписи
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 { 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
// Ожидаем установки соединения
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 BtcLikeAttachSignatureRequest = {
network?: 'livenet' | 'testnet';
publicKey: string;
type?: 'p2pkh' | 'p2wpkh';
utxo: { txid: string; vout: number; value: string }[];
to: string;
amount: string;
fee: string;
signatures: { inputIndex: number; signature: EcdsaSignature }[];
};
type BtcLikeAttachSignatureResponse = {
requestId: string;
data: { txdata: string };
};
export const btcLikeAttachSignature = async (
auth: AuthorizationSession,
chain: BTCLikeChain,
{ network, publicKey, type, utxo, to, amount, fee, signatures }: BtcLikeAttachSignatureRequest,
): Promise<BtcLikeAttachSignatureResponse['data']> => {
const token = await auth.getPermissionToken();
const response = await axios.post(
`https://cloud.spatium.net/blockchain-connector-btc-like/v1/api/transaction/attach-signature/${chain}`,
{
publicKey,
network,
type,
utxo,
to,
amount,
fee,
signatures,
},
{
headers: {
'request-id': uuid(randomBytes),
'authorization': `Bearer ${token}`,
},
},
).then((result) => result.data);
return response.data;
};
struct BtcLikeAttachSignatureRequest: Encodable {
let network: Network?
let publicKey: String
let type: AddressType?
let utxo: [UTXO]
let to: String
let amount: String
let fee: String
let signatures: [Signature]
}
struct UTXO: Encodable {
let txid: String
let vout: Int
let value: String
}
struct Signature: Encodable {
let inputIndex: Int
let signature: EcdsaSignature
}
struct BtcLikeAttachSignatureResponse: Decodable {
let requestId: String
let data: TransactionData
}
struct TransactionData: Decodable {
let txdata: String
}
func btcLikeAttachSignature(auth: AuthorizationSession,
chain: BTCLikeChain,
request: BtcLikeAttachSignatureRequest) async throws -> TransactionData {
let token = try await auth.getPermissionToken()
var urlRequest = URLRequest(url: URL(string: "https://cloud.spatium.net/blockchain-connector-btc-like/v1/api/transaction/attach-signature/\(chain)")!)
urlRequest.httpMethod = "POST"
urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
urlRequest.setValue("Bearer \(token)", forHTTPHeaderField: "authorization")
urlRequest.setValue(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(BtcLikeAttachSignatureResponse.self, from: data)
return response.data
}
Отправка транзакции в сеть
За отправку транзакции в блокчейн также отвечает Blockchain Connector Service
import axios from 'axios';
import { randomBytes, uuid } from '@spatium/sdk';
type BtcLikeSendTXRequest = {
network?: 'livenet' | 'testnet';
txdata: string;
};
type BtcLikeSendTXResponse = {
requestId: string;
data: { txid: string };
};
export const btcLikeSendTX = async (
auth: AuthorizationSession,
chain: BTCLikeChain,
{ network, txdata }: BtcLikeSendTXRequest,
): Promise<BtcLikeSendTXResponse> => {
const token = await auth.getPermissionToken();
const response = await axios.post(
`https://cloud.spatium.net/blockchain-connector-btc-like/v1/api/transaction/send/${chain}`,
{
network,
txdata,
},
{
headers: {
'request-id': uuid(randomBytes),
'authorization': `Bearer ${token}`,
},
}
).then((result) => result.data);
return response.data;
};
struct BtcLikeSendTXRequest: Encodable {
let network: String?
let txdata: String
}
struct BtcLikeSendTXResponse: Decodable {
let requestId: String
let data: TransactionIdData
}
struct TransactionIdData: Decodable {
let txid: String
}
func btcLikeSendTX(auth: AuthorizationSession,
chain: BTCLikeChain,
request: BtcLikeSendTXRequest) async throws -> BtcLikeSendTXResponse {
let token = try await auth.getPermissionToken()
var urlRequest = URLRequest(url: URL(string: "https://cloud.spatium.net/blockchain-connector-btc-like/v1/api/transaction/send/\(chain)")!)
urlRequest.httpMethod = "POST"
urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")
urlRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
urlRequest.setValue(UUID().uuidString, forHTTPHeaderField: "request-id")
let data = try JSONEncoder().encode(request)
urlRequest.httpBody = data
let (responseData, _) = try await URLSession.shared.data(for: urlRequest)
print("btcLikeSendTX response json \(String(data: responseData, encoding: .utf8) ?? "NADA")")
let response = try JSONDecoder().decode(BtcLikeSendTXResponse.self, from: responseData)
return response
}
Полная процедура
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 btcLikeSignTransaction = async (auth: AuthorizationSession, signerClient: SignerClient, syncSessionId: string,
chain: BTCLikeChain, publicKey: string, to: string, amount: string) => {
const { address } = await btcLikeGetAddress(auth, 'btc', { publicKey });
const { utxo } = await btcLikeGetUTXO(auth, 'btc', { address });
const { size } = await btcLikeGetTXSize(auth, 'btc', { publicKey, utxo, to, amount });
const pricePerByte = await btcLikeGetBytePrices(auth, {});
const fee = (Number(pricePerByte.btc?.feeInfo.normal) * size).toFixed();
const { hashes } = await btcLikeGetHash(auth, 'btc', { publicKey, utxo, to, amount, fee });
const signatures: { inputIndex: number; signature: EcdsaSignature }[] = [];
for (const { inputIndex, hash } of hashes) {
const signingToken = await getSigningToken(hash)
const signature = await signEcdsa(signerClient, syncSessionId, hash, signingToken);
signatures.push({ inputIndex, signature });
}
const txdata = await btcLikeAttachSignature(auth, 'btc', { publicKey, utxo, to, amount, fee, signatures });
const txid = await btcLikeSendTX(auth, 'btc', { 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"])
let url = URL(string: "https://cloud.spatium.net/authorization/v1/api/security-factor/credentials")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
guard let body = try? JSONEncoder().encode(["username": "username", "password": "password"]) else {
throw CustomError.configurationError
}
request.httpBody = body
request.addValue(UUID().uuidString, forHTTPHeaderField: "Content-Type")
let (data,_) = try await URLSession.shared.data(for: request)
// получение security токенов
let securityToken = try JSONDecoder().decode(SecurityTokenResponse.self, from: data).data.securityToken
try await auth.establish([securityToken])
struct Signature : Codable {
let inputIndex : Int?
let signature : EcdsaSignature?
}
func btcLikeSignTransaction(auth: AuthorizationSession, secretId: String, signerClient: SignerClient, syncSessionId: String, chain: BTCLikeChain, publicKey: String, to: String, amount: String) async throws -> String {
let address = try await btcLikeGetAddress(auth: auth, chain: chain, addressRequest: BtcLikeGetAddressRequest(network: .testnet, publicKey: publicKey, type: .p2wpkh, prefix: false))
let utxo = try await btcLikeGetUTXO(auth: auth, chain: .btc, request: BtcLikeGetUTXORequest(network: .testnet, address: address ?? ""))
let sizeResponse = try await btcLikeGetTXSize(auth: auth, chain: .btc, sizeRequest: BtcLikeGetTXSizeRequest2(network: .testnet, publicKey: publicKey, type: .p2wpkh, utxo: utxo, to: to, amount: amount))
let size = sizeResponse.size
let priceResponse = try await btcLikeGetBytePrices(auth: auth, request: BtcLikeGetBytePricesRequest(network: .testnet))
let pricePerByte = priceResponse[BTCLikeChain.btc.rawValue]?.feeInfo.normal ?? "0"
let fee = (Double(pricePerByte)! * Double(size)).rounded().description
guard let hashes = try await btcLikeGetHash(auth: auth, chain: .btc, request: BtcLikeGetHashRequest(publicKey: publicKey, utxo: utxo, to: to, amount: amount, fee: fee)) else {
throw CustomError.codingError
}
var signatures = [Signature]()
for hash in hashes {
let inputIndex = hash.inputIndex
let hash = hash.hash
let signingToken = try await getSigningToken(message: hash)
let signature = try await signEcdsa(signerClient: signerClient, secretId: secretId, syncSessionId: syncSessionId, message: hash, signingToken: signingToken)
signatures.append(Signature(inputIndex: inputIndex, signature: signature))
}
let txdata = try await btcLikeAttachSignature(auth: auth,
chain: .btc,
request: BtcLikeAttachSignatureRequest(network: .testnet,
publicKey: publicKey,
type: .p2wpkh,
utxo: utxo,
to: to,
amount: amount,
fee: fee,
signatures: signatures
)
)
let txid = try await btcLikeSendTX(auth: auth, chain: .btc, request: BtcLikeSendTXRequest(network: .testnet, txdata: txdata.txdata))
return txid.data.txid
}