Self-Custody Use Case (BTC-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:
- auth - authorization session (AuthorizationSession)
- crypto - any valid Crypto to handle data and computations
import { uuid, randomBytes, MemoryStorageDriver, SpatiumCrypto } from '@spatium/sdk';
import { AuthorizationSession, ServiceStorage, SignerClient } from '@spatium/signer-client';
export const createSignerClient = () => {
const auth = new AuthorizationSession('https://cloud.spatium.net/authorization/v1', uuid(randomBytes), ['read', 'secret']);
const storage = new ServiceStorage('https://cloud.spatium.net/storage/v1', auth);
const cache = new MemoryStorageDriver()
const crypto = new SpatiumCrypto(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 storage, which stores both secrets at Spatium. Using 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.
Synchronizing Currency Addresses
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 the 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 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/blockchain-connector-btc-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(BtcLikeGetAddressResponse.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 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
}
Transaction Signing
Transaction signing includes several steps:
- Gathering current data from a blockchain, such as UTXO or nonce.
- Estimating a transaction fee.
- Generating a transaction hash.
- Signing a transaction hash.
- Creating a signed transaction.
- Sending the transaction to the blockchain.
Of all these stages, only the signing of the transaction hash is performed using the SDK, the rest is provided through the Blockchain Connector Service API.
UTXO Gathering
Bitcoin-like currencies require up-to-date Unspent Transaction Outputs (UTXO) for transaction formation.
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
}
Transaction Size Estimation
To estimate the transaction fee in Bitcoin-like blockchains, the transaction size in bytes is used, which requires knowledge of UTXO as well.
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
}
Getting Information about an Average Network Commission
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;
};
enum Network: String, Codable {
case livenet
case testnet
}
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, network: Network) 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-\(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
}
Calculating Transaction Fee
The final value of the optimal fee is determined by the transaction size and the average cost per byte. Additionally, you can manually input the total fee amount, disregarding both the size and estimation.
Transaction Hash
With all the preliminary data, it is possible to form a signing hash (in this case, a set of hashes).
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
}
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 Hash Signature
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 a Signed Transaction
A signature (in this case, a set of signatures) needs to be attached to a transaction, thereby obtaining data ready to be sent to the blockchain.
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
}
Sending a Transaction to the Network
Blockchain Connector Service is also responsible for sending the transaction to a blockchain
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
}
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 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)
// get security tokens
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
}