Перейти к содержанию

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
}

Расчет величины комиссии

Финальная величина оптимальной комиссии формируется из размера транзакции и средней стоимости одного байта. Также можно ввести общую сумму комиссии вручную, игнорируя и размер, и оценку.

const fee = (Number(prices.normal) * size).toFixed();
let normalFee = Double(prices.feeInfo.normal)! 
let fee = (normalFee * Double(size)).rounded()

Формирование хэша для подписи

Имея все предварительные данные, можно формировать хэш для подписи (в данном случае - набор хэшей).

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
}