/** * Native RGB bindings for Node.js * * This module provides a bridge to the orbis1-rgb-lib native bindings, * maintaining API compatibility with the React Native Turbo Module interface from orbis1-sdk-rn. * * @module core/NativeRgb */ import * as RgbLib from 'orbis1-rgb-lib'; import * as fs from 'fs'; import * as path from 'path'; import type { InvoiceData, Transfer, Unspent, AssetMetadata, AssetCfa, AssetIfa, AssetNia, } from './Interfaces'; import { normalizeInvoiceData } from './Interfaces'; /** * Get the RGB data directory path. * Returns the .orbis1-wallet-data folder at the project root level (same level as node_modules/). * This ensures wallet data survives node_modules cleanup operations. * * Project structure: * my-project/ * ├─ node_modules/ * │ └─ orbis1-sdk-node/ (this package) * └─ .orbis1-wallet-data/ (wallet data - survives npm install/clean) */ function getRgbDataDir(): string { // Find project root by walking up from node_modules/orbis1-sdk-node/dist/core/ // to the directory that contains node_modules/ let currentDir = __dirname; // dist/core/ let projectRoot = currentDir; // Walk up until we find node_modules or reach filesystem root while (currentDir !== path.dirname(currentDir)) { const parentDir = path.dirname(currentDir); const nodeModulesPath = path.join(parentDir, 'node_modules'); if (fs.existsSync(nodeModulesPath)) { projectRoot = parentDir; break; } currentDir = parentDir; } // Use hidden directory to avoid clutter in project root const dataDir = path.join(projectRoot, '.orbis1-wallet-data'); // Ensure directory exists if (!fs.existsSync(dataDir)) { fs.mkdirSync(dataDir, { recursive: true }); } return dataDir; } /** * Wallet registry to maintain wallet instances by ID */ class WalletRegistry { private wallets = new Map(); private onlineStates = new Map(); private masterFingerprints = new Map(); private dataDirs = new Map(); private nextId = 1; register(wallet: RgbLib.Wallet, masterFingerprint?: string, dataDir?: string): number { const id = this.nextId++; this.wallets.set(id, wallet); if (masterFingerprint) { this.masterFingerprints.set(id, masterFingerprint); } if (dataDir) { this.dataDirs.set(id, dataDir); } return id; } get(id: number): RgbLib.Wallet { const wallet = this.wallets.get(id); if (!wallet) { throw new Error(`Wallet with ID ${id} not found`); } return wallet; } getMasterFingerprint(id: number): string | undefined { return this.masterFingerprints.get(id); } getDataDir(id: number): string | undefined { return this.dataDirs.get(id); } getOnline(id: number): any { const online = this.onlineStates.get(id); if (!online) { throw new Error(`Wallet ${id} is not online`); } return online; } setOnline(id: number, online: any): void { this.onlineStates.set(id, online); } dropOnline(id: number): void { this.onlineStates.delete(id); } remove(id: number): void { this.wallets.delete(id); this.onlineStates.delete(id); this.masterFingerprints.delete(id); this.dataDirs.delete(id); } has(id: number): boolean { return this.wallets.has(id); } } const registry = new WalletRegistry(); /** * Maps our BitcoinNetwork enum values to orbis1-rgb-lib format */ function mapNetworkToRgbLib( network: 'MAINNET' | 'TESTNET' | 'TESTNET4' | 'REGTEST' | 'SIGNET' ): 'Mainnet' | 'Testnet' | 'Testnet4' | 'Regtest' | 'Signet' { const mapping: Record< string, 'Mainnet' | 'Testnet' | 'Testnet4' | 'Regtest' | 'Signet' > = { MAINNET: 'Mainnet', TESTNET: 'Testnet', TESTNET4: 'Testnet4', REGTEST: 'Regtest', SIGNET: 'Signet', }; return mapping[network]; } /** * Converts SDK assignment format to Rust enum JSON format required by orbis1-rgb-lib * SDK: { type: 'FUNGIBLE', amount: 100 } -> RGB: { "Fungible": 100 } * SDK: { type: 'ANY' } -> RGB: "Any" */ function mapAssignmentToRgbLib(assignment: { type: 'FUNGIBLE' | 'NON_FUNGIBLE' | 'INFLATION_RIGHT' | 'REPLACE_RIGHT' | 'ANY'; amount?: number; }): any { const typeMap: Record = { 'FUNGIBLE': 'Fungible', 'NON_FUNGIBLE': 'NonFungible', 'INFLATION_RIGHT': 'InflationRight', 'REPLACE_RIGHT': 'ReplaceRight', 'ANY': 'Any', }; const mappedType = typeMap[assignment.type] || assignment.type; if (assignment.amount !== undefined) { // With amount: { "Fungible": 100 } return { [mappedType]: assignment.amount }; } else { // Without amount: "Any" return mappedType; } } export interface Spec { generateKeys( bitcoinNetwork: 'MAINNET' | 'TESTNET' | 'TESTNET4' | 'REGTEST' | 'SIGNET' ): Promise<{ mnemonic: string; xpub: string; accountXpubVanilla: string; accountXpubColored: string; masterFingerprint: string; }>; restoreKeys( bitcoinNetwork: 'MAINNET' | 'TESTNET' | 'TESTNET4' | 'REGTEST' | 'SIGNET', mnemonic: string ): Promise<{ mnemonic: string; xpub: string; accountXpubVanilla: string; accountXpubColored: string; masterFingerprint: string; }>; restoreBackup(path: string, password: string): Promise; initializeWallet( dataDir: string | null, network: string, accountXpubVanilla: string, accountXpubColored: string, mnemonic: string | null, masterFingerprint: string, supportedSchemas: string[], maxAllocationsPerUtxo: number, vanillaKeychain: number ): Promise; goOnline( walletId: number, skipConsistencyCheck: boolean, indexerUrl: string ): Promise; createOnline( walletId: number, skipConsistencyCheck: boolean, indexerUrl: string ): Promise; setOnline(walletId: number, online: any): Promise; dropOnline(walletId: number, online?: any): Promise; getBtcBalance( walletId: number, skipSync: boolean ): Promise<{ vanilla: { settled: number; future: number; spendable: number; }; colored: { settled: number; future: number; spendable: number; }; }>; walletClose(walletId: number): Promise; backup(walletId: number, backupPath: string, password: string): Promise; backupInfo(walletId: number): Promise; blindReceive( walletId: number, assetId: string | null, assignment: { type: | 'FUNGIBLE' | 'NON_FUNGIBLE' | 'INFLATION_RIGHT' | 'REPLACE_RIGHT' | 'ANY'; amount?: number; }, durationSeconds: number | null, transportEndpoints: string[], minConfirmations: number ): Promise<{ invoice: string; recipientId: string; expirationTimestamp: number | null; batchTransferIdx: number; }>; createUtxos( walletId: number, upTo: boolean, num: number | null, size: number | null, feeRate: number, skipSync: boolean ): Promise; createUtxosBegin( walletId: number, upTo: boolean, num: number | null, size: number | null, feeRate: number, skipSync: boolean ): Promise; createUtxosEnd( walletId: number, signedPsbt: string, skipSync: boolean ): Promise; deleteTransfers( walletId: number, batchTransferIdx: number | null, noAssetOnly: boolean ): Promise; drainTo( walletId: number, address: string, destroyAssets: boolean, feeRate: number ): Promise; drainToBegin( walletId: number, address: string, destroyAssets: boolean, feeRate: number ): Promise; drainToEnd(walletId: number, signedPsbt: string): Promise; failTransfers( walletId: number, batchTransferIdx: number | null, noAssetOnly: boolean, skipSync: boolean ): Promise; finalizePsbt(walletId: number, signedPsbt: string): Promise; getAddress(walletId: number): Promise; getAssetBalance( walletId: number, assetId: string ): Promise<{ settled: number; future: number; spendable: number; }>; getAssetMetadata(walletId: number, assetId: string): Promise; getFeeEstimation(walletId: number, blocks: number): Promise; getMediaDir(walletId: number): Promise; getWalletData(walletId: number): Promise<{ dataDir: string; bitcoinNetwork: string; databaseType: string; maxAllocationsPerUtxo: number; accountXpubVanilla: string; accountXpubColored: string; mnemonic?: string; masterFingerprint: string; vanillaKeychain?: number; supportedSchemas: string[]; }>; getWalletDir(walletId: number): Promise; inflate( walletId: number, assetId: string, inflationAmounts: number[], feeRate: number, minConfirmations: number ): Promise<{ txid: string; batchTransferIdx: number; }>; inflateBegin( walletId: number, assetId: string, inflationAmounts: number[], feeRate: number, minConfirmations: number ): Promise; inflateEnd( walletId: number, signedPsbt: string ): Promise<{ txid: string; batchTransferIdx: number; }>; issueAssetCfa( walletId: number, name: string, details: string | null, precision: number, amounts: number[], filePath: string | null ): Promise; issueAssetIfa( walletId: number, ticker: string, name: string, precision: number, amounts: number[], inflationAmounts: number[], replaceRightsNum: number, rejectListUrl: string | null ): Promise; issueAssetNia( walletId: number, ticker: string, name: string, precision: number, amounts: number[] ): Promise; issueAssetUda( walletId: number, ticker: string, name: string, details: string | null, precision: number, mediaFilePath: string | null, attachmentsFilePaths: string[] ): Promise<{ assetId: string; ticker: string; name: string; details?: string; precision: number; timestamp: number; addedAt: number; balance: { settled: number; future: number; spendable: number; }; media?: { filePath: string; mime: string; }; attachments: Array<{ filePath: string; mime: string; }>; }>; listAssets( walletId: number, filterAssetSchemas: string[] ): Promise<{ nia: Array<{ assetId: string; ticker: string; name: string; details?: string; precision: number; issuedSupply: number; timestamp: number; addedAt: number; balance: { settled: number; future: number; spendable: number; }; media?: { filePath: string; mime: string; digest: string; }; }>; uda: Array<{ assetId: string; ticker: string; name: string; details?: string; precision: number; timestamp: number; addedAt: number; balance: { settled: number; future: number; spendable: number; }; token?: { index: number; ticker?: string; name?: string; details?: string; embeddedMedia: boolean; media?: { filePath: string; mime: string; digest: string; }; attachments: Array<{ key: number; filePath: string; mime: string; digest: string; }>; reserves: boolean; }; }>; cfa: Array<{ assetId: string; name: string; details?: string; precision: number; issuedSupply: number; timestamp: number; addedAt: number; balance: { settled: number; future: number; spendable: number; }; media?: { filePath: string; mime: string; digest: string; }; }>; ifa: Array<{ assetId: string; ticker: string; name: string; details?: string; precision: number; initialSupply: number; maxSupply: number; knownCirculatingSupply: number; timestamp: number; addedAt: number; balance: { settled: number; future: number; spendable: number; }; media?: { filePath: string; mime: string; digest: string; }; rejectListUrl?: string; }>; }>; listTransactions( walletId: number, skipSync: boolean ): Promise< Array<{ transactionType: 'RGB_SEND' | 'DRAIN' | 'CREATE_UTXOS' | 'USER'; txid: string; received: number; sent: number; fee: number; confirmationTime?: number; }> >; listTransfers(walletId: number, assetId: string | null): Promise; listUnspents( walletId: number, settledOnly: boolean, skipSync: boolean ): Promise; refresh( walletId: number, assetId: string | null, filter: Array<{ status: 'WAITING_COUNTERPARTY' | 'WAITING_CONFIRMATIONS'; incoming: boolean; }>, skipSync: boolean ): Promise<{ [key: string]: { updatedStatus?: | 'WAITING_COUNTERPARTY' | 'WAITING_CONFIRMATIONS' | 'SETTLED' | 'FAILED'; failure?: string; }; }>; send( walletId: number, recipientMap: { [key: string]: Array<{ recipientId: string; witnessData?: { amountSat: number; blinding?: number; }; assignment: { type: | 'FUNGIBLE' | 'NON_FUNGIBLE' | 'INFLATION_RIGHT' | 'REPLACE_RIGHT' | 'ANY'; amount?: number; }; transportEndpoints: string[]; }>; }, donation: boolean, feeRate: number, minConfirmations: number, skipSync: boolean ): Promise<{ txid: string; batchTransferIdx: number; }>; sendBegin( walletId: number, recipientMap: { [key: string]: Array<{ recipientId: string; witnessData?: { amountSat: number; blinding?: number; }; assignment: { type: | 'FUNGIBLE' | 'NON_FUNGIBLE' | 'INFLATION_RIGHT' | 'REPLACE_RIGHT' | 'ANY'; amount?: number; }; transportEndpoints: string[]; }>; }, donation: boolean, feeRate: number, minConfirmations: number, externalInputs: Array<{ txid: string; vout: number; value: number; scriptPubkey: string; }> | null, externalOutputs: Array<{ address: string; value: number; }> | null ): Promise; sendBtc( walletId: number, address: string, amount: number, feeRate: number, skipSync: boolean ): Promise; sendBtcBegin( walletId: number, address: string, amount: number, feeRate: number, skipSync: boolean ): Promise; sendBtcEnd( walletId: number, signedPsbt: string, skipSync: boolean ): Promise; sendEnd( walletId: number, signedPsbt: string, skipSync: boolean ): Promise<{ txid: string; batchTransferIdx: number; }>; signPsbt(walletId: number, unsignedPsbt: string): Promise; sync(walletId: number): Promise; witnessReceive( walletId: number, assetId: string | null, assignment: { type: | 'FUNGIBLE' | 'NON_FUNGIBLE' | 'INFLATION_RIGHT' | 'REPLACE_RIGHT' | 'ANY'; amount?: number; }, durationSeconds: number | null, transportEndpoints: string[], minConfirmations: number ): Promise<{ invoice: string; recipientId: string; expirationTimestamp: number | null; batchTransferIdx: number; }>; verifyConsignmentAmount( walletId: number, consignmentPath: string, recipientId: string, expectedAmount: string ): Promise; decodeInvoice(invoice: string): Promise; } /** * Implementation of the Spec interface using orbis1-rgb-lib */ const RgbNative: Spec = { async generateKeys(bitcoinNetwork) { return RgbLib.generateKeys(mapNetworkToRgbLib(bitcoinNetwork)); }, async restoreKeys(bitcoinNetwork, mnemonic) { return RgbLib.restoreKeys(mapNetworkToRgbLib(bitcoinNetwork), mnemonic); }, async restoreBackup(path, password) { const dataDir = getRgbDataDir(); return RgbLib.restoreBackup(path, password, dataDir); }, async initializeWallet( dataDir, network, accountXpubVanilla, accountXpubColored, mnemonic, masterFingerprint, supportedSchemas, maxAllocationsPerUtxo, vanillaKeychain ) { // Use provided dataDir or fall back to default const effectiveDataDir = dataDir ?? getRgbDataDir(); // Ensure the data directory exists if (!fs.existsSync(effectiveDataDir)) { fs.mkdirSync(effectiveDataDir, { recursive: true }); } // Clean up stale runtime lock file if present // This prevents the wallet from getting stuck if a previous process didn't exit cleanly const walletDir = path.join(effectiveDataDir, masterFingerprint); const lockFile = path.join(walletDir, 'rgb_runtime.lock'); if (fs.existsSync(lockFile)) { try { fs.unlinkSync(lockFile); } catch (err) { // Ignore errors if we can't delete the lock file // The RGB library will handle it on initialization } } // Map our SDK enums to RGB lib enums const bitcoinNetwork = mapNetworkToRgbLib(network as any); const databaseType = RgbLib.DatabaseType.Sqlite; // Map schema strings to RGB lib enum values const mappedSchemas = supportedSchemas?.map(schema => { const schemaMap: Record = { 'NIA': RgbLib.AssetSchema.Nia, 'UDA': RgbLib.AssetSchema.Uda, 'CFA': RgbLib.AssetSchema.Cfa, 'IFA': RgbLib.AssetSchema.Ifa, }; return schemaMap[schema] || schema; }); const walletData = new RgbLib.WalletData({ dataDir: effectiveDataDir, bitcoinNetwork, databaseType, accountXpubVanilla, accountXpubColored, maxAllocationsPerUtxo: String(maxAllocationsPerUtxo), vanillaKeychain: String(vanillaKeychain ?? 0), // Must be '0' as string, not undefined ...(mnemonic != null && { mnemonic }), masterFingerprint, supportedSchemas: mappedSchemas, }); const wallet = new RgbLib.Wallet(walletData); return registry.register(wallet, masterFingerprint, effectiveDataDir); }, async goOnline(walletId, skipConsistencyCheck, indexerUrl) { const wallet = registry.get(walletId); const online = wallet.goOnline(skipConsistencyCheck, indexerUrl); registry.setOnline(walletId, online); return online; }, async createOnline(walletId, skipConsistencyCheck, indexerUrl) { const wallet = registry.get(walletId); const online = wallet.goOnline(skipConsistencyCheck, indexerUrl); // Don't store in registry - return for explicit control return online; }, async setOnline(walletId, online) { registry.setOnline(walletId, online); }, async dropOnline(walletId: number, online?: any) { if (online) { // Explicit online provided: drop the connection // Only remove from registry if it matches the stored one try { const registryOnline = registry.getOnline(walletId); if (registryOnline === online) { registry.dropOnline(walletId); } } catch (error) { // Wallet might not have online in registry, that's okay } await RgbLib.dropOnline(online); } else { // No online provided: get from registry and drop it try { const registryOnline = registry.getOnline(walletId); await RgbLib.dropOnline(registryOnline); registry.dropOnline(walletId); } catch (error) { // Wallet might not be online, that's okay - just ensure it's removed from registry registry.dropOnline(walletId); } } }, async getBtcBalance(walletId, skipSync) { const wallet = registry.get(walletId); const online = registry.getOnline(walletId); return wallet.getBtcBalance(online, skipSync); }, async walletClose(walletId) { registry.remove(walletId); }, async backup(walletId, backupPath, password) { const wallet = registry.get(walletId); return wallet.backup(backupPath, password); }, async backupInfo(walletId) { const wallet = registry.get(walletId); return wallet.backupInfo(); }, async blindReceive( walletId, assetId, assignment, durationSeconds, transportEndpoints, minConfirmations ) { const wallet = registry.get(walletId); return wallet.blindReceive( assetId, JSON.stringify(mapAssignmentToRgbLib(assignment)), durationSeconds !== null ? String(durationSeconds) : null, transportEndpoints, String(minConfirmations) ); }, async createUtxos(walletId, upTo, num, size, feeRate, skipSync) { const wallet = registry.get(walletId); const online = registry.getOnline(walletId); return wallet.createUtxos( online, upTo, num !== null ? String(num) : null, size !== null ? String(size) : null, String(feeRate), skipSync ); }, async createUtxosBegin(walletId, upTo, num, size, feeRate, skipSync) { const wallet = registry.get(walletId); const online = registry.getOnline(walletId); return wallet.createUtxosBegin( online, upTo, num !== null ? String(num) : null, size !== null ? String(size) : null, String(feeRate), skipSync ); }, async createUtxosEnd(walletId, signedPsbt, skipSync) { const wallet = registry.get(walletId); const online = registry.getOnline(walletId); return wallet.createUtxosEnd(online, signedPsbt, skipSync); }, async deleteTransfers(walletId, batchTransferIdx, noAssetOnly) { const wallet = registry.get(walletId); return wallet.deleteTransfers(batchTransferIdx, noAssetOnly); }, async drainTo(_walletId, _address, _destroyAssets, _feeRate) { // drainTo methods are not available in the current wrapper throw new Error('drainTo is not supported in this version of orbis1-rgb-lib'); }, async drainToBegin(_walletId, _address, _destroyAssets, _feeRate) { throw new Error('drainToBegin is not supported in this version of orbis1-rgb-lib'); }, async drainToEnd(_walletId, _signedPsbt) { throw new Error('drainToEnd is not supported in this version of orbis1-rgb-lib'); }, async failTransfers(_walletId, _batchTransferIdx, _noAssetOnly, _skipSync) { throw new Error('failTransfers is not supported in this version of orbis1-rgb-lib'); }, async finalizePsbt(_walletId, _signedPsbt) { throw new Error('finalizePsbt is not supported in this version of orbis1-rgb-lib'); }, async getAddress(walletId) { const wallet = registry.get(walletId); return wallet.getAddress(); }, async getAssetBalance(walletId, assetId) { const wallet = registry.get(walletId); return wallet.getAssetBalance(assetId); }, async getAssetMetadata(walletId, assetId) { const wallet = registry.get(walletId); return wallet.getAssetMetadata(assetId); }, async getFeeEstimation(walletId, blocks) { const wallet = registry.get(walletId); const online = registry.getOnline(walletId); return wallet.getFeeEstimation(online, String(blocks)); }, async getMediaDir(_walletId) { throw new Error('getMediaDir is not supported in this version of orbis1-rgb-lib'); }, async getWalletData(_walletId) { throw new Error('getWalletData is not supported in this version of orbis1-rgb-lib'); }, async getWalletDir(walletId) { // Use the dataDir that was stored during wallet initialization const dataDir = registry.getDataDir(walletId) ?? getRgbDataDir(); const masterFingerprint = registry.getMasterFingerprint(walletId); if (!masterFingerprint) { // Fallback to data dir if no masterFingerprint stored return dataDir; } return path.join(dataDir, masterFingerprint); }, async inflate(_walletId, _assetId, _inflationAmounts, _feeRate, _minConfirmations) { throw new Error('inflate is not supported in this version of orbis1-rgb-lib'); }, async inflateBegin( _walletId, _assetId, _inflationAmounts, _feeRate, _minConfirmations ) { throw new Error('inflateBegin is not supported in this version of orbis1-rgb-lib'); }, async inflateEnd(_walletId, _signedPsbt) { throw new Error('inflateEnd is not supported in this version of orbis1-rgb-lib'); }, async issueAssetCfa(walletId, name, details, precision, amounts, filePath) { const wallet = registry.get(walletId); return wallet.issueAssetCFA( name, details, String(precision), amounts.map(String), filePath ); }, async issueAssetIfa( _walletId, _ticker, _name, _precision, _amounts, _inflationAmounts, _replaceRightsNum, _rejectListUrl ) { // IFA is not supported in the current version of orbis1-rgb-lib wrapper throw new Error('issueAssetIfa is not supported in this version of orbis1-rgb-lib'); }, async issueAssetNia(walletId, ticker, name, precision, amounts) { const wallet = registry.get(walletId); return wallet.issueAssetNIA( ticker, name, String(precision), amounts.map(String) ); }, async issueAssetUda( walletId, ticker, name, details, precision, mediaFilePath, attachmentsFilePaths ) { const wallet = registry.get(walletId); return wallet.issueAssetUDA( ticker, name, details, String(precision), mediaFilePath, attachmentsFilePaths ); }, async listAssets(walletId, filterAssetSchemas) { const wallet = registry.get(walletId); // Map schema names from SDK format (NIA) to wrapper format (Nia) const mappedSchemas = filterAssetSchemas.map(schema => { const schemaMap: Record = { 'NIA': 'Nia', 'UDA': 'Uda', 'CFA': 'Cfa', 'IFA': 'Ifa', }; return schemaMap[schema] || schema; }); return wallet.listAssets(mappedSchemas); }, async listTransactions(walletId, skipSync) { const wallet = registry.get(walletId); const online = registry.getOnline(walletId); return wallet.listTransactions(online, skipSync); }, async listTransfers(walletId, assetId) { const wallet = registry.get(walletId); return wallet.listTransfers(assetId); }, async listUnspents(walletId, settledOnly, skipSync) { const wallet = registry.get(walletId); const online = registry.getOnline(walletId); return wallet.listUnspents(online, settledOnly, skipSync); }, async refresh(walletId, assetId, filter, skipSync) { const wallet = registry.get(walletId); const online = registry.getOnline(walletId); // Convert filter objects to strings as expected by wrapper.js const filterStrings = filter.map(f => JSON.stringify(f)); return wallet.refresh(online, assetId, filterStrings, skipSync); }, async send( walletId, recipientMap, donation, feeRate, minConfirmations, skipSync ) { const wallet = registry.get(walletId); const online = registry.getOnline(walletId); // Convert assignments to Rust enum format (as objects, not strings) // wrapper.js will JSON.stringify the entire recipientMap const mappedRecipientMap: any = {}; for (const [assetId, recipients] of Object.entries(recipientMap)) { mappedRecipientMap[assetId] = recipients.map((recipient: any) => ({ ...recipient, assignment: mapAssignmentToRgbLib(recipient.assignment), })); } return wallet.send( online, mappedRecipientMap, donation, String(feeRate), String(minConfirmations), skipSync ); }, async sendBegin( walletId, recipientMap, donation, feeRate, minConfirmations, externalInputs, externalOutputs ) { const wallet = registry.get(walletId); const online = registry.getOnline(walletId); // Convert assignments to Rust enum format (as objects, not strings) // wrapper.js will JSON.stringify the entire recipientMap const mappedRecipientMap: any = {}; for (const [assetId, recipients] of Object.entries(recipientMap)) { mappedRecipientMap[assetId] = recipients.map((recipient: any) => ({ ...recipient, assignment: mapAssignmentToRgbLib(recipient.assignment), })); } // Transform external inputs from camelCase to snake_case for Rust // The SDK API uses scriptPubkey (camelCase) but Rust expects script_pubkey (snake_case) const transformedExternalInputs = externalInputs?.map((input: any) => ({ txid: input.txid, vout: input.vout, value: input.value, script_pubkey: input.scriptPubkey, })) ?? null; return wallet.sendBegin( online, mappedRecipientMap, donation, String(feeRate), String(minConfirmations), transformedExternalInputs, externalOutputs ); }, async sendBtc(walletId, address, amount, feeRate, skipSync) { const wallet = registry.get(walletId); const online = registry.getOnline(walletId); return wallet.sendBtc(online, address, String(amount), String(feeRate), skipSync); }, async sendBtcBegin(_walletId, _address, _amount, _feeRate, _skipSync) { // sendBtcBegin is not available in the current wrapper throw new Error('sendBtcBegin is not supported in this version of orbis1-rgb-lib'); }, async sendBtcEnd(_walletId, _signedPsbt, _skipSync) { // sendBtcEnd is not available in the current wrapper throw new Error('sendBtcEnd is not supported in this version of orbis1-rgb-lib'); }, async sendEnd(walletId, signedPsbt, skipSync) { const wallet = registry.get(walletId); const online = registry.getOnline(walletId); return wallet.sendEnd(online, signedPsbt, skipSync); }, async signPsbt(walletId, unsignedPsbt) { const wallet = registry.get(walletId); return wallet.signPsbt(unsignedPsbt); }, async sync(walletId) { const wallet = registry.get(walletId); const online = registry.getOnline(walletId); return wallet.sync(online); }, async witnessReceive( walletId, assetId, assignment, durationSeconds, transportEndpoints, minConfirmations ) { const wallet = registry.get(walletId); return wallet.witnessReceive( assetId, JSON.stringify(mapAssignmentToRgbLib(assignment)), durationSeconds !== null ? String(durationSeconds) : null, transportEndpoints, String(minConfirmations) ); }, async verifyConsignmentAmount( walletId: number, consignmentPath: string, recipientId: string, expectedAmount: string ) { const wallet = registry.get(walletId); return wallet.verifyConsignmentAmount( consignmentPath, recipientId, expectedAmount ); }, async decodeInvoice(invoice) { const rawData = RgbLib.invoiceData(invoice); return normalizeInvoiceData(invoice, rawData); }, }; export default RgbNative;