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

Self-Custody Use Case (ETH-like)

This guide illustrates the creation of secrets, address synchronization, balance retrieval, and transaction sending from a client wallet application within Self-Custody using Spatium Signer Service.

Initialization

To interact with Spatium Signer Client, you need to provide the following data:

import { MemoryStorageDriver, SpatiumCrypto } from '@spatium/sdk';
import { AuthorizationSession, ServiceStorage, SignerClient } from '@spatium/signer-client';

export const createSignerClient = (auth: AuthorizationSession) => {
    const storage = new ServiceStorage('https://cloud.spatium.net/storage/v1', auth);

    const cache = new MemoryStorageDriver()
    const crypto = new 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
}

Important! In the Typescript example, ServiceStorage is used as a storage, which places both secrets at Spatium. The use of this format implies that the wallet is custodial. It's not recommended for production use.

Secret Generation

To use a distributed wallet, you need to generate a permanent pair of client and server secrets and ensure their secure storage. On the Spatium Signer Service side, secret management is automated, while on the client side, the developer must implement a stableStorageDriver on their own. In both cases, the secret is bound to its identifier (secretId) and is accessible through it after creation. A user can have any number of secrets, but to ensure their security and recoverability, it is recommended to use one secret per user.

To backup secrets in case of StorageDriver content loss, it is recommended to use export and import features.

export const ensureSecret = async (signerClient: SignerClient, secretId: string) => {
  if (await signerClient.crypto.checkSecret(secretId)) {
    return;
  }

  try {
    // Wait for the actual connection to be established
    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;
    }

    // Wait for the actual connection to be established
    try await signerClient.connect(timeout: 10 * 1000);
    defer {
        signerClient.disconnect()
    }
    try await signerClient.generateDistributedSecret(secretId: secretId);
};

Important! At this stage of SDK development, it is recommended to use a similar approach when interacting with the service, i.e., connecting immediately before interaction and disconnecting afterwards. This will help to avoid common network errors until they are fully resolved.

Currency Address Synchronization

A currency address is required to receive assets and request balance information, so it is recommended to synchronize it immediately when creating a wallet.

To reduce synchronization time and improve user experience, it is recommended to use a single public key (sync parameters) for all currencies within the same cryptographic system. To do so, first synchronize one public key for the corresponding cryptographic system, and then generate desired currencies' addresses with it.

Each new synchronization procedure is bound to a unique identifier, and its results are recorded in the provided StorageDriver, which allows (with a saved syncSessionId) to synchronize the key once and then use the results permanently. However, loss of the synchronization data does not have long-term consequences, as it is possible to perform synchronization again and obtain the same public key and addresses.

The following data is required for the synchronization procedure:

  • secretId - identifier of the secret, serving as the entropy for this wallet. Secrets must have already been generated by the time of synchronization;
  • syncSessionId - synchronization session identifier. In case of a match, the previous session with such identifier will be overwritten;
  • curve - the elliptic curve. For all currencies of this example it is secp256k1;
  • derivationCoin - HD key derivation parameter that directly affects the address generation result. Unique values lead to the generation of unique keys. It is recommended to use a fixed value for a specific cryptographic system and vary the key value using the next parameter;
  • derivationAccount - HD key derivation parameter that directly affects the address generation result. Unique values lead to the generation of unique keys.
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 {
    // Wait for the actual connection to be established
    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
    }

    // Wait for the actual connection to be established
    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
}

To obtain an address in a specific blockchain from a public key, it is recommended to use 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
}

Important! In order to be able to restore all addresses with user funds in case of data loss in the StorageDriver, it is necessary to ensure backup of a client secret and an external storage of address generation parameters, specifically:

  • secretId - generation data, also needs to be stored along with the secret backup
  • curve - generation data
  • derivationCoin - generation data
  • derivationAccount - generation data

Retrieving Address Information

Having a synchronized address (or any other address), you can access the Address Info Service to retrieve detailed information about the address, including various assets' balances and transaction history.

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
}

Transaction Signing

Transaction signing includes several steps:

  • Gathering current data from a blockchain, such as UTXO or nonce.
  • Estimating a transaction fee.
  • Forming a transaction hash.
  • Signing a transaction hash.
  • Forming a signed transaction.
  • Sending the transaction to the blockchain.

Of all these stages, only the signing of the transaction hash is performed using SDK, the rest is provided through the Blockchain Connector Service API.

Gathering Nonce

Ethereum-like currencies require a current nonce (a one-time transaction number) to form a transaction.

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
}

Gathering ERC-20 Token Transfer Data

Important! This method is only used when sending ERC-20 tokens, and when sending the native currency of the blockchain, this method is not used, and an empty string is sent in the data parameter of subsequent requests for working with the transaction.

To send ERC-20 tokens, it is necessary to obtain corresponding transfer data, this data is passed in the data field of subsequent requests for working with the transaction.

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
}

Gathering ERC-20 Token Approve Data

Important! This method is only used when approving ERC-20 tokens, and when sending the native currency of the blockchain, this method is not used, and an empty string is sent in the data parameter of subsequent requests for working with the transaction.

To approve ERC-20 tokens, it is necessary to obtain corresponding approve data, this data is passed in the data field of subsequent requests for working with the transaction.

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
}

Estimating the Workload for Transaction Processing

In Ethereum-like blockchains, the workload performed by the validator for transaction processing is used to estimate the transaction fee, for which it is also necessary to know the 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
}

Obtaining Information on the Average Network Fee

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
}

Calculating Transaction Fee

The final amount of an optimal fee is formed from the workload performed by the validator for transaction processing and the average cost of one unit of work.

const fee = (Number(gasPrices.normal) * gasLimit).toFixed();
let fee = (Double(gasPrices.normal)! * Double(gasLimit)!).rounded().description

However, within the API, calculating this value is not required, as the gasLimit and gasPrice transaction parameters are passed directly.

Transaction hash

With all the preliminary data, it is possible to form the hash for signing.

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
}

Getting signingToken

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 Signing of the Hash

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 {
    // Wait for the actual connection to be established
    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
    }
}

Forming the Signed Transaction

A signature needs to be attached to the transaction, thereby obtaining data ready to be sent to the blockchain.

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
}

Sending a Transaction to the Network

Blockchain Connector Service is also responsible for sending transactions to a blockchain.

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
}

Complete Procedure

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']);

// get security tokens
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 });

  // getting erc20 transfer data
  // const { erc20TransferData } = await erc20GetTransfer(auth, { to, amount });

  // getting erc20 approve data
  // const { erc20ApproveData } = await erc20GetApprove(auth, { to, amount });

  const { gasLimit } = await ethLikeGetGasLimit(auth, 'eth', { to, amount, data: '', nonce });

  const { gasPrices } = await ethLikeGetGasPrices(auth, 'eth', {});

  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
}