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

Self-Custody Use Case (XLM)

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, 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
}

Important! In the TS 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 xlm it is ed25519;
  • 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 { syncDistributedEddsaKey, getEddsaPublicKey } from '@@spatium/sdk';

export const ensureEddsaPublicKey = async (signerClient: SignerClient, secretId: string, syncSessionId: string, derivationCoin: number, derivationAccount: number): Promise<string> => {
  const publicKey = await getEddsaPublicKey(signerClient, secretId, syncSessionId).catch(() => null);
  if (publicKey) {
    return publicKey;
  }

  try {
    // Wait for the actual connection to be established 
    await signerClient.connect(10 * 1000);

    const distributedEddsaKey = await syncDistributedEddsaKey(signerClient, secretId, syncSessionId, 'ed25519', derivationCoin, derivationAccount);
    return distributedEddsaKey
  } finally {
    await signerClient.disconnect();
  }
};
func ensureEddsaPublicKey(signerClient: SignerClient, secretId: String, syncSessionId: String, derivationCoin: UInt32, derivationAccount: UInt32) async throws -> String {

    let publicKey = try await getEddsaPublicKey(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 distributedEddsaKey = try await syncDistributedEddsaKey(
        driver: signerClient,
        secretId: secretId,
        syncSessionId: syncSessionId,
        curve: .ed25519,
        derivationCoin: derivationCoin,
        derivationAccount: derivationAccount
    )

    return distributedEddsaKey
}

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 cGetAddressRequest = {
  network?: 'livenet' | 'testnet';
  publicKey: string;
  prefix?: boolean;
};
type XlmGetAddressResponse = {
  requestId: string;
  data: { address: string };
}

export const xlmGetAddress = async (
  auth: AuthorizationSession,
  { network, publicKey, prefix }: XlmGetAddressRequest,
): Promise<XlmGetAddressResponse['data']> => {
  const token = await auth.getPermissionToken();

  const response = await axios.post(
    `https://cloud.spatium.net/blockchain-connector-xlm/v1/api/get-address/xlm`,
    {
      publicKey,
      network,
      prefix,
    },
    {
      headers: {
        'request-id': uuid(randomBytes),
        'authorization': `Bearer ${token}`,
      },
    },
  ).then((result) => result.data);

  return response.data;
};
struct XlmGetAddressRequest: Codable {
    enum Network: String, Codable {
        case livenet
        case testnet
    }

    let network: Network?
    let publicKey: String
    let prefix: Bool?
}

struct XlmGetAddressResponse: Decodable {
    let requestId: String
    let data: DataResponse
}

struct DataResponse: Decodable {
    let address: String
}

func xlmGetAddress(auth: AuthorizationSession, addressRequest: XlmGetAddressRequest) async throws -> String? {
    guard let url = URL(string: "https://cloud.spatium.net/blockchain-connector-xlm/v1/api/get-address") 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(XlmGetAddressResponse.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 XlmGetAddressRequest = {
  network?: 'livenet' | 'testnet';
  address: string;
};
type XlmGetAddressResponse = {
  requestId: string;
  data: AddressInfo;
};

export const xlmGetAddressInfo = async (
  auth: AuthorizationSession,
  { network, address }: XlmGetAddressRequest,
): Promise<XlmGetAddressResponse['data']> => {
  const token = await auth.getPermissionToken();

  const result = await axios.get(
    `https://cloud.spatium.net/address-info-xlm/v1/api/xlm`,
    {
      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 XlmGetAddressInfoRequest {
    enum Network: String, Codable {
        case livenet
        case testnet
    }
    let network: Network?
    let address: String
}

struct XlmGetAddressInfoResponse: Codable {
    let requestId: String
    let data: AddressInfo
}

func btcLikeGetAddressInfo(auth: AuthorizationSession,
                           addressInfoRequest: XlmGetAddressInfoRequest) async throws -> AddressInfo {
    let token = try await auth.getPermissionToken()
    var url = URLComponents(string: "https://cloud.spatium.net/address-info-xlm/v1/api/xlm")!
    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(XlmGetAddressInfoResponse.self, from: data)

    return result.data
}

Transaction Signing

Transaction signing includes several steps:

  • 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.

Getting Fee Info

To provide the correct fee size, it is useful to address current fee statistics.

Note

There are different maximum fee forming strategies. But one should take into account that a transaction with a low maximum fee won't be processed with a high network load, so we recommend using the statistically highest fee with the addition of a small backup - current minimal fee per operation (feeCharged.max + lastLedgerBaseFee), at the same time leaving the option to enter any fee value manually. More info about network fee you can find here.

import axios from 'axios';
import { randomBytes, uuid } from '@@spatium/sdk';

type XlmGetFeeInfoResponse = {
type XlmGetFeeStatResponse = {
  xlm: {
    lastLedgerBaseFee: string;
    feeCharged: {
      min: string,
      max: string,
    }
  }
};

export const xlmGetFeeInfo = async (
  network: 'livenet' | 'testnet',
): Promise<XlmGetFeeInfoResponse> => {
  const result = xlmGetFeeInfoResponseSchema.parse(await axios.get(
    `https://cloud.spatium.net/fee-info-xlm/v1/static/fee-info-${network}.json`,
    {
    },
  ).then((result) => result.data));

  return result;
};
enum Network: String, Codable {
    case livenet
    case testnet
}

struct XlmFeeInfo: Codable {
  let lastLedgerBaseFee: String
  let feeCharged: FeeCharged

struct FeeCharged: Codable {
    let min: String
    let max: String
  }
}

struct XlmGetFeeInfoResponse: Codable {
  let xlm: XlmFeeInfo
}

func xlmGetFeeInfo(network: Network) async throws -> XlmGetFeeInfoResponse {
    let url = URL(string: "https://cloud.spatium.net/fee-info-xlm/v1/static/fee-info-\(network.rawValue).json")!

    let (data, response) = try await URLSession.shared.data(from: url)

    guard (response as? HTTPURLResponse)?.statusCode == 200 else {
        throw URLError(.badServerResponse)
    }

    let result = try JSONDecoder().decode(XlmGetFeeInfoResponse.self, from: data)

    return result
}

Transaction hash

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

In XLM blockchain getting hash depends on transaction type:

  • Transfer
  • Adding trustline
  • Claim

Getting Transaction Hash for Transfer

Important

According to Stellar logic, one can't transfer XLM to a non-existent account (it is necessary to create one first). This endpoint will automatically create the account if it's necessary, but there will be no notice about account absence! More info about getting a transaction hash for transfer is here.

import axios from 'axios';
import { randomBytes, uuid } from '@@spatium/sdk';

type XlmGetHashTransferRequest = {
  network?: 'livenet' | 'testnet';
  publicKey: string;
  to: string,
  amount: string,
  asset?: StellarAsset;
  fee: string;
  memo?: string;
 };

type XlmGetHashTransferResponse = {
  requestId: string;
  data: {
    unsignedMessage: string;
    hash: string;
  }
};

export const xlmGetHashTransfer = async (
  auth: AuthorizationSession,
  { network, publicKey, to, amount, asset, fee, memo }: XlmGetHashTransferRequest,
): Promise<XlmGetHashTransferResponse> => {
  const token = await auth.getPermissionToken();

  const response = await axios.post(
    'https://cloud.spatium.net/blockchain-connector-xlm/v1/api/transaction/get-hash/xlm/transfer',
    {
      network,
      publicKey,
      to,
      amount,
      asset,
      fee,
      memo,
    },
    {
      headers: {
        'request-id': uuid(randomBytes),
        'authorization': `Bearer ${token}`,
      },
    },
  ).then((result) => result.data);

  return response.data;
};
struct StellarAsset: Encodable {
    let kind = "stellar-asset"
    let chain = "xlm"
    let code: String
    let issuer: String
}

struct XlmGetHashTransferRequest: Encodable {
    let network: Network?
    let publicKey: String
    let to: String
    let amount: String
    let asset: StellarAsset?
    let fee: String
    let memo: String?
}

struct XlmGetHashTransferResponse: Decodable {
    let unsignedMessage: String
    let hash: String
}

func xlmGetHashTransfer(
  request: XlmGetHashTransferRequest
) async throws -> XlmGetHashTransferResponse {

  guard let url = URL(string: "https://cloud.spatium.net/blockchain-connector-xlm/v1/api/transaction/get-hash/xlm/transfer") else {
    throw CustomError.urlError
  }

  let auth = try AuthManager.shared.getAuthPermission()

  let token = try await auth.getPermissionToken()

  let headers = [
    "request-id": UUID().uuidString,
    "authorization": "Bearer \(token)"
  ]

  let parameters = request

  var urlRequest = URLRequest(url: url)
  urlRequest.httpMethod = "POST"
  headers.forEach { key, value in
    urlRequest.addValue(value, forHTTPHeaderField: key)
  }
  urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type")

  guard let body = try? JSONEncoder().encode(parameters) else {
    throw CustomError.configurationError
  }

  urlRequest.httpBody = body

  let (data, _) = try await URLSession.shared.data(for: urlRequest)

  guard let response = try? JSONDecoder().decode(XlmGetHashTransferResponse.self, from: data) else {
    throw CustomError.codingError
  }

  return response
}

Getting Transaction Hash for Trustline Adding

import axios from 'axios';
import { randomBytes, uuid } from '@@spatium/sdk';

type XlmGetHashAddTrustlineRequest = {
  network?: 'livenet' | 'testnet';
  publicKey: string;
  to: string,
  amount?: string,
  asset: StellarAsset;
  fee: string;
  memo?: string;
 };

type XlmGetHashAddTrustlineResponse = {
  requestId: string;
  data: {
    unsignedMessage: string;
    hash: string;
  }
};

export const xlmGetHashAddTrustline = async (
  auth: AuthorizationSession,
  { network, publicKey, to, amount, asset, fee, memo }: XlmGetHashAddTrustlineRequest,
): Promise<XlmGetHashAddTrustlineResponse> => {
  const token = await auth.getPermissionToken();

  const response = await axios.post(
    'https://cloud.spatium.net/blockchain-connector-xlm/v1/api/transaction/get-hash/xlm/add-trustline',
    {
      network,
      publicKey,
      to,
      amount,
      asset,
      fee,
      memo,
    },
    {
      headers: {
        'request-id': uuid(randomBytes),
        'authorization': `Bearer ${token}`,
      },
    },
  ).then((result) => result.data);

  return response.data;
};
struct XlmGetHashAddTrustlineRequest: Encodable {
    let network: String?
    let publicKey: String
    let to: String
    let amount: String?
    let asset: StellarAsset
    let fee: String
    let memo: String?
}

struct XlmGetHashAddTrustlineResponse: Decodable {
    let requestId: String
    let data: XlmGetHashAddTrustlineResponseData
}

struct XlmGetHashAddTrustlineResponseData: Decodable {
    let unsignedMessage: String
    let hash: String
}

func xlmGetHashAddTrustline(auth: AuthorizationSession, request: XlmGetHashAddTrustlineRequest) async throws -> XlmGetHashAddTrustlineResponse {

    let token = try await auth.getPermissionToken()

    let url = URL(string: "https://cloud.spatium.net/blockchain-connector-xlm/v1/api/transaction/get-hash/xlm/add-trustline")!

    var urlRequest = URLRequest(url: url)
    urlRequest.httpMethod = "POST"
    urlRequest.setValue(UUID().uuidString, forHTTPHeaderField: "request-id")
    urlRequest.setValue("Bearer \(token)", forHTTPHeaderField: "authorization")

    let encoder = JSONEncoder()
    let body = try encoder.encode(request)
    urlRequest.httpBody = body

    let (data, _) = try await URLSession.shared.data(for: urlRequest)

    guard let response = try? JSONDecoder().decode(XlmGetHashAddTrustlineResponse.self, from: data) else {
        throw CustomError.codingError
    }
    return response
}

Getting Transaction Hash for Claim

import axios from 'axios';
import { randomBytes, uuid } from '@@spatium/sdk';

type XlmGetHashClaimRequest = {
  network?: 'livenet' | 'testnet';
  publicKey: string;
  claimableBalanceId: string,
  fee: string;
  memo?: string;
 };

type XlmGetHashClaimResponse = {
  requestId: string;
  data: {
    unsignedMessage: string;
    hash: string;
  }
};

export const xlmGetHashClaim = async (
  auth: AuthorizationSession,
  { network, publicKey, claimableBalanceId, fee, memo }: XlmGetHashClaimRequest,
): Promise<XlmGetHashClaimResponse> => {
  const token = await auth.getPermissionToken();

  const response = await axios.post(
    'https://cloud.spatium.net/blockchain-connector-xlm/v1/api/transaction/get-hash/xlm/claim',
    {
      network,
      publicKey,
      claimableBalanceId,
      fee,
      memo,
    },
    {
      headers: {
        'request-id': uuid(randomBytes),
        'authorization': `Bearer ${token}`,
      },
    },
  ).then((result) => result.data);

  return response.data;
};
struct XlmGetHashClaimRequest: Encodable {
  let network: String?
  let publicKey: String
  let claimableBalanceId: String
  let fee: String
  let memo: String?
}

struct XlmGetHashClaimResponse: Decodable {
  let requestId: String
  let data: XlmGetHashClaimResponseData
}

struct XlmGetHashClaimResponseData: Decodable {
  let unsignedMessage: String
  let hash: String
}

func xlmGetHashClaim(auth: AuthorizationSession, request: XlmGetHashClaimRequest) async throws -> XlmGetHashClaimResponse {

  let token = try await auth.getPermissionToken()

  let url = URL(string: "https://cloud.spatium.net/blockchain-connector-xlm/v1/api/transaction/get-hash/xlm/claim")!

  var urlRequest = URLRequest(url: url)
  urlRequest.httpMethod = "POST"
  urlRequest.setValue(UUID().uuidString, forHTTPHeaderField: "request-id")
  urlRequest.setValue("Bearer \(token)", forHTTPHeaderField: "authorization")

  let encoder = JSONEncoder()
  let body = try encoder.encode(request)
  urlRequest.httpBody = body

  let (data, _) = try await URLSession.shared.data(for: urlRequest)

  let response = try JSONDecoder().decode(XlmGetHashClaimResponse.self, from: data)
  return response
}

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 { signEddsaMessage } from '@@spatium/sdk';
import { randomBytes, uuid } from '@@spatium/sdk';

export const signEddsa = async (signerClient: SignerClient, secretId: string, syncSessionId: string, message: string, signingToken: string): Promise<EddsaSignature> => {
  const signSessionId = uuid(randomBytes);

  try {
    // Wait for the actual connection to be established
    await signerClient.connect(10 * 1000);

    return await signerClient.signEddsaMessage(secretId, syncSessionId, signSessionId, message, signingToken);
  } finally {
    await signerClient.disconnect();
  }
};
func signEddsa(signerClient: SignerClient,
               secretId: String,
               syncSessionId: String,
               message: String,
               signingToken: String) async throws -> EddsaSignature {

    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.signEddsaMessage(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 XlmAttachSignatureRequest = {
  network?: 'livenet' | 'testnet';
  publicKey: string;
  unsignedMessage: string;
  signature: EddsaSignature,
};

type XlmAttachSignatureResponse = {
  requestId: string;
  data: { txdata: string };
};

export const xlmAttachSignature = async (
  auth: AuthorizationSession,
  { network, publicKey, unsignedMessage, signature }: XlmAttachSignatureRequest,
): Promise<XlmAttachSignatureResponse['data']> => {

  const response = await axios.post(
    `https://cloud.spatium.net/blockchain-connector-xlm/v1/api/transaction/attach-signature/xlm`,
    {
      network,
      publicKey,
      unsignedMessage,
      signature,
    },
    {
      headers: {
        'request-id': uuid(randomBytes),
        'authorization': `Bearer ${token}`,
      },
    },
  ).then((result) => result.data);

  return response.data;
};
struct XlmAttachSignatureRequest: Encodable {
    enum Network: String, Codable {
        case livenet
        case testnet
    }

    let network: Network?
    let publicKey: String
    let unsignedMessage: String
    let signature: EddsaSignature
}

struct XlmAttachSignatureResponse: Decodable {
    let requestId: String
    let data: TxData
}

struct TxData: Decodable {
    let txdata: String
}

func xlmAttachSignature(auth: AuthorizationSession,
                            request: XlmAttachSignatureRequest) async throws -> TxData {

    let token = try await auth.getPermissionToken()

    let url = URL(string: "https://cloud.spatium.net/blockchain-connector-xlm/v1/api/transaction/attach-signature/xlm")!

    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(XlmAttachSignatureResponse.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 XlmSendTXRequest = {
  txdata: string;
};
type XlmSendTXResponse = {
  requestId: string;
  data: { txid: string };
};

export const xlmSendTX = async (
  auth: AuthorizationSession,
  { txdata }: XlmSendTXRequest,
): Promise<XlmSendTXResponse['data']> => {
  const token = await auth.getPermissionToken();

  const response = await axios.post(
    `https://cloud.spatium.net/blockchain-connector-xlm/v1/api/transaction/send/xlm`,
    {
      txdata,
    },
    {
      headers: {
        'request-id': uuid(randomBytes),
        'authorization': `Bearer ${token}`,
      },
    }
  ).then((result) => result.data);

  return response.data;
};
struct XlmSendTXRequest: Encodable {
    let txdata: String
}

struct XlmSendTXResponse: Decodable {
    let requestId: String
    let data: TXID
}

struct TXID: Decodable {
    let txid: String
}

func xlmSendTX(auth: AuthorizationSession,
                   request: XlmSendTXRequest) async throws -> TXID {

    let token = try await auth.getPermissionToken()

    let url = URL(string: "https://cloud.spatium.net/blockchain-connector-xlm/v1/api/transaction/send/xlm")!

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

// getting 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 xlmSignTransaction = async (auth: AuthorizationSession, signerClient: SignerClient, syncSessionId: string, publicKey: string, to: string, amount: string) => {

  const { address } = await xlmGetAddress(auth, { publicKey })

  const feeInfo = await xlmGetFeeInfo(network);
  const fee = (Number(feeInfo.xlm.feeCharged.max) + Number(feeInfo.xlm.lastLedgerBaseFee)).toString();

  // getting transfer transaction hash
  const { hash, unsignedMessage } = await xlmGetHashTransfer(auth, { publicKey, to, amount, fee });

  // getting add trustline transaction hash
  // const { hash, unsignedMessage } = await xlmGetHashAddTrustline(auth, { publicKey, asset, fee });

  // getting claim transaction hash
  // const { hash, unsignedMessage } = await xlmGetHashClaim(auth, { publicKey, claimableBalanceId, fee });

  const signingToken = await getSigningToken(hash)

  const signature = await signEddsa(signer, secretId, syncSessionId, hash, signingToken);

  const { txdata } = await xlmAttachSignature(auth, { publicKey, unsignedMessage, signature });

  const { txid } = await xlmSendTX(auth, { txdata });

  return txid;
}
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])
//    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
}

func signXlmTransaction(auth: AuthorizationSession, signerClient: SignerClient, syncSessionId: String, publicKey: String, to: String, amount: String) async throws -> String {
    // let address = try await xlmGetAddress(auth: auth, addressRequest: XlmGetAddressRequest(network: .testnet, publicKey: publicKey, prefix: false))

    let feeInfo = try await xlmGetFeeInfo(network: .testnet)
    let fee = (feeInfo.xlm.feeCharged.max + feeInfo.xlm.lastLedgerBaseFee).description

    let hashResult = try await xlmGetHashTransfer(request:
                                                    XlmGetHashTransferRequest(
                                                        network: .testnet,
                                                        publicKey: publicKey,
                                                        to: to,
                                                        amount: amount,
                                                        asset: nil,
                                                        fee: fee,
                                                        memo: nil
                                                    )
    )

    let hash = hashResult.hash
    let unsignedMessage = hashResult.unsignedMessage

    let signingToken = try await getSigningToken(message: hash)

    guard let secretId = AuthManager.shared.secretID else {
        throw CustomError.configurationError
    }

    let signature = try await signEddsa(signerClient: signerClient, secretId: secretId, syncSessionId: syncSessionId, message: hash, signingToken: signingToken)

    let attachResult = try await xlmAttachSignature(auth: auth, request: XlmAttachSignatureRequest(network: .testnet, publicKey: publicKey, unsignedMessage: unsignedMessage, signature: signature))
    let txdata = attachResult.txdata

    let sendResult = try await xlmSendTX(auth: auth, request: XlmSendTXRequest(txdata: txdata))
    return sendResult.txid
}