nexa-wallet-sdk 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.parcel-cache/3e09f086f3c4d605-AssetGraph +0 -0
- package/.parcel-cache/5eac57ec674cdae8-AssetGraph +0 -0
- package/.parcel-cache/data.mdb +0 -0
- package/.parcel-cache/e43547b6c9167b58-RequestGraph +0 -0
- package/.parcel-cache/ecfe15d74834bbfd-BundleGraph +0 -0
- package/.parcel-cache/lock.mdb +0 -0
- package/.parcel-cache/snapshot-e43547b6c9167b58.txt +2 -0
- package/README.md +445 -0
- package/dist/browser/index.js +2456 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/index.d.ts +918 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2915 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2456 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +90 -0
- package/spec.md +257 -0
- package/src/index.ts +93 -0
- package/src/models/rostrum.entities.ts +159 -0
- package/src/models/transaction.entities.ts +46 -0
- package/src/models/wallet.entities.ts +42 -0
- package/src/network/RostrumProvider.ts +137 -0
- package/src/types.ts +0 -0
- package/src/utils/CommonUtils.ts +123 -0
- package/src/utils/TXUtils.ts +445 -0
- package/src/utils/TokenUtils.ts +75 -0
- package/src/utils/ValidationUtils.ts +86 -0
- package/src/utils/WalletUtils.ts +522 -0
- package/src/utils/WatchOnlyTXUtils.ts +275 -0
- package/src/wallet/Wallet.ts +397 -0
- package/src/wallet/WatchOnlyWallet.ts +169 -0
- package/src/wallet/accounts/AccountStore.ts +173 -0
- package/src/wallet/accounts/interfaces/BaseAccountInterface.ts +56 -0
- package/src/wallet/accounts/models/DappAccount.ts +80 -0
- package/src/wallet/accounts/models/DefaultAccount.ts +96 -0
- package/src/wallet/accounts/models/VaultAccount.ts +81 -0
- package/src/wallet/transactions/WalletTransactionCreator.ts +145 -0
- package/src/wallet/transactions/WatchOnlyTransactionCreator.ts +189 -0
- package/src/wallet/transactions/interfaces/TransactionCreator.ts +438 -0
- package/tests/core/tx/transactioncreator.test.ts +455 -0
- package/tests/core/tx/wallettransactioncreator.test.ts +362 -0
- package/tests/core/tx/watchonlytransactioncreator.test.ts +258 -0
- package/tests/core/wallet/accountstore.test.ts +341 -0
- package/tests/core/wallet/wallet.test.ts +69 -0
- package/tests/core/watchonlywallet/watchonlywallet.test.ts +251 -0
- package/tests/index.test.ts +12 -0
- package/tsconfig.json +113 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import {ElectrumClient, SubscribeCallback} from "@vgrunner/electrum-cash";
|
|
2
|
+
import {
|
|
3
|
+
BlockTip,
|
|
4
|
+
IFirstUse,
|
|
5
|
+
IListUnspentRecord,
|
|
6
|
+
ITXHistory,
|
|
7
|
+
ITokensBalance,
|
|
8
|
+
ITokenListUnspent,
|
|
9
|
+
ITransaction,
|
|
10
|
+
IUtxo,
|
|
11
|
+
RostrumParams,
|
|
12
|
+
ITokenGenesis,
|
|
13
|
+
RostrumScheme, RostrumTransportScheme
|
|
14
|
+
} from "../models/rostrum.entities";
|
|
15
|
+
import { Balance } from "../models/wallet.entities";
|
|
16
|
+
|
|
17
|
+
type RPCParameter = string | number | boolean | null;
|
|
18
|
+
|
|
19
|
+
export class RostrumProvider {
|
|
20
|
+
|
|
21
|
+
private client?: ElectrumClient;
|
|
22
|
+
|
|
23
|
+
public constructor() {}
|
|
24
|
+
|
|
25
|
+
public async getVersion() {
|
|
26
|
+
return await this.execute<string[]>('server.version');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public async getBlockTip() {
|
|
30
|
+
return await this.execute<BlockTip>('blockchain.headers.tip');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public async getBalance(address: string) {
|
|
34
|
+
return await this.execute<Balance>('blockchain.address.get_balance', address, 'exclude_tokens');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public async getTransactionsHistory(address: string) {
|
|
38
|
+
return await this.execute<ITXHistory[]>('blockchain.address.get_history', address);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
public async getFirstUse(address: string) {
|
|
42
|
+
return await this.execute<IFirstUse>('blockchain.address.get_first_use', address);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public async getTransaction(id: string, verbose: boolean = true) {
|
|
46
|
+
return await this.execute<ITransaction>('blockchain.transaction.get', id, verbose);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public async getUtxo(outpoint: string) {
|
|
50
|
+
return await this.execute<IUtxo>('blockchain.utxo.get', outpoint);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public async getNexaUtxos(address: string) {
|
|
54
|
+
return await this.execute<IListUnspentRecord[]>('blockchain.address.listunspent', address, 'exclude_tokens');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public async getTokenUtxos(address: string, token: string) {
|
|
58
|
+
let listunspent = await this.execute<ITokenListUnspent>('token.address.listunspent', address, null, token);
|
|
59
|
+
return listunspent.unspent;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public async getTokensBalance(address: string, token?: string) {
|
|
63
|
+
if (token) {
|
|
64
|
+
return await this.execute<ITokensBalance>('token.address.get_balance', address, null, token);
|
|
65
|
+
}
|
|
66
|
+
return await this.execute<ITokensBalance>('token.address.get_balance', address);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public async getTokenGenesis(token: string) {
|
|
70
|
+
return await this.execute<ITokenGenesis>('token.genesis.info', token);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public async subscribeToAddresses(addresses:string[], callback: SubscribeCallback) {
|
|
74
|
+
for (const addr of addresses) {
|
|
75
|
+
await this.client?.subscribe(callback, 'blockchain.address.subscribe', addr)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public async broadcast(txHex: string) {
|
|
80
|
+
return await this.execute<string>('blockchain.transaction.broadcast', txHex);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
public async getLatency() {
|
|
84
|
+
try {
|
|
85
|
+
let start = Date.now();
|
|
86
|
+
let res = await this.getBlockTip();
|
|
87
|
+
if (res) {
|
|
88
|
+
return Date.now() - start;
|
|
89
|
+
}
|
|
90
|
+
return 0;
|
|
91
|
+
} catch {
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
public async connect(params?: RostrumParams) {
|
|
97
|
+
try {
|
|
98
|
+
if (!params) {
|
|
99
|
+
params = {
|
|
100
|
+
host: 'aus.electrum.nexa.onethirtyseven.dev',
|
|
101
|
+
port: 30004,
|
|
102
|
+
scheme: (RostrumScheme.WSS as RostrumTransportScheme)
|
|
103
|
+
}
|
|
104
|
+
// params = await StorageProvider.getRostrumParams();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
this.client = new ElectrumClient("com.otoplo.wallet", "1.4.3", params.host, params.port, params.scheme, 30*1000, 10*1000, true);
|
|
108
|
+
await this.client.connect();
|
|
109
|
+
} catch (e) {
|
|
110
|
+
if (e instanceof Error) {
|
|
111
|
+
console.info(e.message);
|
|
112
|
+
} else {
|
|
113
|
+
console.error(e);
|
|
114
|
+
}
|
|
115
|
+
throw e;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
public async disconnect(force?: boolean) {
|
|
120
|
+
try {
|
|
121
|
+
return await this.client!.disconnect(force);
|
|
122
|
+
} catch (e) {
|
|
123
|
+
console.log(e)
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private async execute<T>(method: string, ...parameters: RPCParameter[]) {
|
|
129
|
+
var res = await this.client!.request(method, ...parameters);
|
|
130
|
+
if (res instanceof Error) {
|
|
131
|
+
throw res;
|
|
132
|
+
}
|
|
133
|
+
return res as T;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export const rostrumProvider = new RostrumProvider();
|
package/src/types.ts
ADDED
|
File without changes
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import bigDecimal from 'js-big-decimal';
|
|
2
|
+
import { Address, CommonUtils } from 'libnexa-ts';
|
|
3
|
+
|
|
4
|
+
/** Maximum value for a 64-bit signed integer */
|
|
5
|
+
export const MAX_INT64: bigint = 9223372036854775807n;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Get the current Unix timestamp in seconds
|
|
9
|
+
*
|
|
10
|
+
* @returns Current timestamp as number of seconds since Unix epoch
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const now = currentTimestamp();
|
|
15
|
+
* console.log('Current time:', now);
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export function currentTimestamp() {
|
|
19
|
+
return Math.floor(Date.now() / 1000);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if a value is null, undefined, or empty
|
|
24
|
+
*
|
|
25
|
+
* @param arg - The value to check
|
|
26
|
+
* @returns true if the value is null, undefined, empty string, or empty array
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* isNullOrEmpty(''); // true
|
|
31
|
+
* isNullOrEmpty([]); // true
|
|
32
|
+
* isNullOrEmpty(null); // true
|
|
33
|
+
* isNullOrEmpty('hello'); // false
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export function isNullOrEmpty(arg?: string | any[] | null): arg is undefined | [] | null | '' {
|
|
37
|
+
return !arg || arg.length === 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Parse an amount with decimal places and format it for display
|
|
42
|
+
*
|
|
43
|
+
* Converts a raw amount to a human-readable format by dividing by 10^decimals
|
|
44
|
+
* and removing trailing zeros.
|
|
45
|
+
*
|
|
46
|
+
* @param amount - The raw amount to parse
|
|
47
|
+
* @param decimals - Number of decimal places
|
|
48
|
+
* @returns Formatted amount string
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```typescript
|
|
52
|
+
* parseAmountWithDecimals(1000000, 6); // '1'
|
|
53
|
+
* parseAmountWithDecimals(1500000, 6); // '1.5'
|
|
54
|
+
* parseAmountWithDecimals(1000000n, 6); // '1'
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
export function parseAmountWithDecimals(amount: string | number | bigint, decimals: number) {
|
|
58
|
+
let val = new bigDecimal(amount).divide(new bigDecimal(Math.pow(10, decimals)), decimals).getPrettyValue();
|
|
59
|
+
if (val.match(/\./)) {
|
|
60
|
+
val = val.replace(/\.?0+$/, '');
|
|
61
|
+
}
|
|
62
|
+
return val;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Convert a decimal amount to raw integer format
|
|
67
|
+
*
|
|
68
|
+
* Multiplies the amount by 10^decimals to get the raw integer representation
|
|
69
|
+
* used in transactions.
|
|
70
|
+
*
|
|
71
|
+
* @param amount - The decimal amount to convert
|
|
72
|
+
* @param decimals - Number of decimal places
|
|
73
|
+
* @returns Raw amount as string
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```typescript
|
|
77
|
+
* getRawAmount('1.5', 6); // '1500000'
|
|
78
|
+
* getRawAmount(1.5, 6); // '1500000'
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export function getRawAmount(amount: string | number | bigint, decimals: number) {
|
|
82
|
+
return new bigDecimal(amount).multiply(new bigDecimal(Math.pow(10, decimals))).getValue();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get the buffer representation of an address
|
|
87
|
+
*
|
|
88
|
+
* @param address - The address string (hex or Nexa address format)
|
|
89
|
+
* @returns Buffer containing the address data
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```typescript
|
|
93
|
+
* const buffer = getAddressBuffer('nexatest:nqtsq5g5jsdmqqywaqd82lhnnk3a8wqunjz6gtxdtavnnekc');
|
|
94
|
+
* const hexBuffer = getAddressBuffer('a1b2c3d4e5f6');
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
export function getAddressBuffer(address: string) {
|
|
98
|
+
if (CommonUtils.isHexa(address)) {
|
|
99
|
+
return Buffer.from(address, 'hex') ;
|
|
100
|
+
}
|
|
101
|
+
return Address.fromString(address).data;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Convert a token ID to hex format
|
|
106
|
+
*
|
|
107
|
+
* @param token - The token ID (hex string or Nexa address format)
|
|
108
|
+
* @returns Token ID in hex format
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```typescript
|
|
112
|
+
* const hexId = tokenIdToHex('nexatest:tq8r37lcjlqazz7vuvug84q2ev50573hesrnxkv9y6hvhhl5k5qqqnmyf79mx');
|
|
113
|
+
* const hexId2 = tokenIdToHex('a1b2c3d4e5f6'); // already hex, returns as-is
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
export function tokenIdToHex(token: string) {
|
|
117
|
+
if (CommonUtils.isHexa(token)) {
|
|
118
|
+
return token;
|
|
119
|
+
}
|
|
120
|
+
return getAddressBuffer(token).toString('hex');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import {rostrumProvider} from '../network/RostrumProvider';
|
|
2
|
+
import {AccountKeys} from '../models/wallet.entities';
|
|
3
|
+
import {isNullOrEmpty, MAX_INT64, tokenIdToHex} from './CommonUtils';
|
|
4
|
+
import {
|
|
5
|
+
Address,
|
|
6
|
+
AddressType,
|
|
7
|
+
GroupToken,
|
|
8
|
+
Networkish,
|
|
9
|
+
PrivateKey,
|
|
10
|
+
Transaction,
|
|
11
|
+
TransactionBuilder,
|
|
12
|
+
UnitUtils,
|
|
13
|
+
UTXO
|
|
14
|
+
} from "libnexa-ts";
|
|
15
|
+
import {PermissionLabel, TxOptions} from "../models/transaction.entities";
|
|
16
|
+
import {dupAuthority, isAuthFit} from "./TokenUtils";
|
|
17
|
+
|
|
18
|
+
/** Maximum number of inputs/outputs allowed in a single transaction */
|
|
19
|
+
const MAX_INPUTS_OUTPUTS = 250;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Populate a transaction with Nexa UTXO inputs and set change output if needed
|
|
23
|
+
*
|
|
24
|
+
* This function automatically selects and adds UTXOs from the provided account keys
|
|
25
|
+
* to satisfy the transaction's Nexa amount requirements. It handles change calculation
|
|
26
|
+
* and can consolidate UTXOs when requested.
|
|
27
|
+
*
|
|
28
|
+
* @param txBuilder - The transaction builder to populate
|
|
29
|
+
* @param keys - Account keys containing receive and change addresses with balances
|
|
30
|
+
* @param totalTxValue - Total amount of Nexa required for the transaction (in satoshis)
|
|
31
|
+
* @param options - Transaction options including consolidation settings and change address
|
|
32
|
+
* @returns Promise resolving to array of private keys needed for signing
|
|
33
|
+
* @throws {Error} If insufficient balance or too many inputs required
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* const txBuilder = new TransactionBuilder();
|
|
38
|
+
* const keys = await account.getKeys();
|
|
39
|
+
* const privateKeys = await populateNexaInputsAndChange(
|
|
40
|
+
* txBuilder,
|
|
41
|
+
* keys,
|
|
42
|
+
* 1000000n, // 1 NEXA
|
|
43
|
+
* { isConsolidate: false, feeFromAmount: false }
|
|
44
|
+
* );
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export async function populateNexaInputsAndChange(txBuilder: TransactionBuilder, keys: AccountKeys, totalTxValue: bigint, options: TxOptions): Promise<PrivateKey[]> {
|
|
48
|
+
let rKeys = keys.receiveKeys.filter(k => BigInt(k.balance) > 0n);
|
|
49
|
+
let cKeys = keys.changeKeys.filter(k => BigInt(k.balance) > 0n);
|
|
50
|
+
let allKeys = rKeys.concat(cKeys);
|
|
51
|
+
if (isNullOrEmpty(allKeys)) {
|
|
52
|
+
throw new Error("Not enough Nexa balance.");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let usedKeys = new Map<string, PrivateKey>();
|
|
56
|
+
let origAmount = options.isConsolidate ? 0 : Number(totalTxValue);
|
|
57
|
+
|
|
58
|
+
for (let key of allKeys) {
|
|
59
|
+
let utxos = await rostrumProvider.getNexaUtxos(key.address);
|
|
60
|
+
for (let utxo of utxos) {
|
|
61
|
+
let input: UTXO = {
|
|
62
|
+
outpoint: utxo.outpoint_hash,
|
|
63
|
+
address: key.address,
|
|
64
|
+
satoshis: utxo.value,
|
|
65
|
+
templateData: options.templateData
|
|
66
|
+
}
|
|
67
|
+
txBuilder.from(input);
|
|
68
|
+
|
|
69
|
+
if (!usedKeys.has(key.address)) {
|
|
70
|
+
usedKeys.set(key.address, key.key.privateKey);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (options.isConsolidate) {
|
|
74
|
+
txBuilder.change(options.toChange ?? keys.receiveKeys[keys.receiveKeys.length - 1].address);
|
|
75
|
+
if (txBuilder.transaction.inputs.length > MAX_INPUTS_OUTPUTS) {
|
|
76
|
+
return Array.from(usedKeys.values());
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
let tx = txBuilder.transaction;
|
|
80
|
+
if (tx.inputs.length > MAX_INPUTS_OUTPUTS) {
|
|
81
|
+
throw new Error("Too many inputs. Consider consolidate transactions or reduce the send amount.");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let unspent = tx.getUnspentValue();
|
|
85
|
+
if (unspent < 0n) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (unspent == 0n && options.feeFromAmount) {
|
|
90
|
+
let txFee = tx.estimateRequiredFee();
|
|
91
|
+
tx.updateOutputAmount(0, origAmount - txFee);
|
|
92
|
+
return Array.from(usedKeys.values());
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
txBuilder.change(options.toChange ?? keys.changeKeys[keys.changeKeys.length - 1].address);
|
|
96
|
+
if (options.feeFromAmount) {
|
|
97
|
+
let hasChange = tx.getChangeOutput();
|
|
98
|
+
let txFee = tx.estimateRequiredFee();
|
|
99
|
+
tx.updateOutputAmount(0, origAmount - txFee);
|
|
100
|
+
|
|
101
|
+
// edge case where change added after update
|
|
102
|
+
if (!hasChange && tx.getChangeOutput()) {
|
|
103
|
+
txFee = tx.estimateRequiredFee();
|
|
104
|
+
tx.updateOutputAmount(0, origAmount - txFee);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// check again after change output manipulation
|
|
109
|
+
if (tx.getUnspentValue() < tx.estimateRequiredFee()) {
|
|
110
|
+
// try to add more utxos to satisfy the minimum fee
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
return Array.from(usedKeys.values());
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (options.isConsolidate) {
|
|
119
|
+
if (usedKeys.size > 0) {
|
|
120
|
+
return Array.from(usedKeys.values());
|
|
121
|
+
}
|
|
122
|
+
throw new Error("Not enough Nexa balance.");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let err = {
|
|
126
|
+
errorMsg: "Not enough Nexa balance.",
|
|
127
|
+
amount: UnitUtils.formatNEXA(txBuilder.transaction.outputs[0].value),
|
|
128
|
+
fee: UnitUtils.formatNEXA(txBuilder.transaction.estimateRequiredFee())
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
throw new Error(JSON.stringify(err));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Populate a transaction with token UTXO inputs and set token change output if needed
|
|
136
|
+
*
|
|
137
|
+
* This function selects and adds token UTXOs from the provided account keys
|
|
138
|
+
* to satisfy the transaction's token amount requirements. It automatically
|
|
139
|
+
* handles token change calculation.
|
|
140
|
+
*
|
|
141
|
+
* @param txBuilder - The transaction builder to populate
|
|
142
|
+
* @param keys - Account keys containing addresses with token balances
|
|
143
|
+
* @param token - The token ID to spend
|
|
144
|
+
* @param outTokenAmount - Amount of tokens required for the transaction
|
|
145
|
+
* @returns Promise resolving to array of private keys needed for signing
|
|
146
|
+
* @throws {Error} If insufficient token balance or too many inputs required
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* ```typescript
|
|
150
|
+
* const txBuilder = new TransactionBuilder();
|
|
151
|
+
* const keys = await account.getKeys();
|
|
152
|
+
* const privateKeys = await populateTokenInputsAndChange(
|
|
153
|
+
* txBuilder,
|
|
154
|
+
* keys,
|
|
155
|
+
* 'token_id_hex',
|
|
156
|
+
* 1000n // Amount of tokens
|
|
157
|
+
* );
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
export async function populateTokenInputsAndChange(txBuilder: TransactionBuilder, keys: AccountKeys, token: string, outTokenAmount: bigint): Promise<PrivateKey[]> {
|
|
161
|
+
let tokenHex = tokenIdToHex(token);
|
|
162
|
+
let rKeys = keys.receiveKeys.filter(k => Object.keys(k.tokensBalance).includes(tokenHex));
|
|
163
|
+
let cKeys = keys.changeKeys.filter(k => Object.keys(k.tokensBalance).includes(tokenHex));
|
|
164
|
+
let allKeys = rKeys.concat(cKeys);
|
|
165
|
+
|
|
166
|
+
if (isNullOrEmpty(allKeys)) {
|
|
167
|
+
throw new Error("Not enough token balance.");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let usedKeys = new Map<string, PrivateKey>();
|
|
171
|
+
let inTokenAmount = 0n;
|
|
172
|
+
|
|
173
|
+
for (let key of allKeys) {
|
|
174
|
+
let utxos = await rostrumProvider.getTokenUtxos(key.address, token);
|
|
175
|
+
for (let utxo of utxos) {
|
|
176
|
+
if (utxo.token_amount < 0) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
txBuilder.from({
|
|
180
|
+
outpoint: utxo.outpoint_hash,
|
|
181
|
+
address: key.address,
|
|
182
|
+
satoshis: utxo.value,
|
|
183
|
+
groupId: utxo.group,
|
|
184
|
+
groupAmount: BigInt(utxo.token_amount),
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
inTokenAmount = inTokenAmount + BigInt(utxo.token_amount);
|
|
188
|
+
if (!usedKeys.has(key.address)) {
|
|
189
|
+
usedKeys.set(key.address, key.key.privateKey);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (inTokenAmount > MAX_INT64) {
|
|
193
|
+
throw new Error("Token inputs exceeded max amount. Consider sending in small chunks");
|
|
194
|
+
}
|
|
195
|
+
if (txBuilder.transaction.inputs.length > MAX_INPUTS_OUTPUTS) {
|
|
196
|
+
throw new Error("Too many inputs. Consider consolidating transactions or reduce the send amount.");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (inTokenAmount == outTokenAmount) {
|
|
200
|
+
return Array.from(usedKeys.values());
|
|
201
|
+
}
|
|
202
|
+
if (inTokenAmount > outTokenAmount) {
|
|
203
|
+
// change
|
|
204
|
+
txBuilder.to(keys.changeKeys[keys.changeKeys.length - 1].address, Transaction.DUST_AMOUNT, token, inTokenAmount - outTokenAmount);
|
|
205
|
+
return Array.from(usedKeys.values());
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
throw new Error("Not enough token balance");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Build a transaction to create a new token group
|
|
215
|
+
*
|
|
216
|
+
* This function creates a group token by using the first UTXO's outpoint
|
|
217
|
+
* to generate a unique group ID. The group token represents the authority
|
|
218
|
+
* to create fungible tokens within this group.
|
|
219
|
+
*
|
|
220
|
+
* @param txBuilder - The transaction builder to populate
|
|
221
|
+
* @param keys - Account keys to use for funding the transaction
|
|
222
|
+
* @param opReturnData - Optional data to include in the group creation
|
|
223
|
+
* @param network - Network to create the group on
|
|
224
|
+
* @returns Promise resolving to array of private keys needed for signing
|
|
225
|
+
* @throws {Error} If insufficient balance for group creation
|
|
226
|
+
*
|
|
227
|
+
* @example
|
|
228
|
+
* ```typescript
|
|
229
|
+
* const txBuilder = new TransactionBuilder();
|
|
230
|
+
* const keys = await account.getKeys();
|
|
231
|
+
* const privateKeys = await buildCreateGroupTransaction(
|
|
232
|
+
* txBuilder,
|
|
233
|
+
* keys,
|
|
234
|
+
* 'my_token_data',
|
|
235
|
+
* Networks.mainnet
|
|
236
|
+
* );
|
|
237
|
+
* ```
|
|
238
|
+
*/
|
|
239
|
+
export async function buildCreateGroupTransaction(
|
|
240
|
+
txBuilder: TransactionBuilder,
|
|
241
|
+
keys: AccountKeys,
|
|
242
|
+
opReturnData: string,
|
|
243
|
+
network: Networkish
|
|
244
|
+
): Promise<PrivateKey[]> {
|
|
245
|
+
// TODO validate opreturn data
|
|
246
|
+
const allKeys = keys.receiveKeys.concat(keys.changeKeys)
|
|
247
|
+
let outpoint = '';
|
|
248
|
+
let usedKeys: PrivateKey[] = [];
|
|
249
|
+
let signKey: PrivateKey | undefined = undefined;
|
|
250
|
+
for (let key of allKeys) {
|
|
251
|
+
let utxos = await rostrumProvider.getNexaUtxos(key.address);
|
|
252
|
+
for (let utxo of utxos) {
|
|
253
|
+
txBuilder.from({
|
|
254
|
+
outpoint: utxo.outpoint_hash,
|
|
255
|
+
address: key.address,
|
|
256
|
+
satoshis: utxo.value
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
if (isNullOrEmpty(outpoint)) {
|
|
260
|
+
outpoint = utxo.outpoint_hash;
|
|
261
|
+
let id = GroupToken.findGroupId(Buffer.from(outpoint, 'hex'), Buffer.from(opReturnData, 'hex'), GroupToken.authFlags.ACTIVE_FLAG_BITS);
|
|
262
|
+
const groupId = new Address(id.hashBuffer, network, AddressType.GroupIdAddress).toString()
|
|
263
|
+
txBuilder.to(keys.receiveKeys.at(-1)!.address, Transaction.DUST_AMOUNT, groupId, GroupToken.authFlags.ACTIVE_FLAG_BITS | id.nonce)
|
|
264
|
+
signKey = key.key.privateKey
|
|
265
|
+
usedKeys.push(signKey);
|
|
266
|
+
return usedKeys
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
throw new Error("Not enough Nexa balance.");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Prepare a transaction to delete/spend a specific UTXO
|
|
276
|
+
*
|
|
277
|
+
* This function adds a specific UTXO as input to the transaction.
|
|
278
|
+
* It's commonly used for spending token authority UTXOs or consolidating specific outputs.
|
|
279
|
+
*
|
|
280
|
+
* @param txBuilder - The transaction builder to populate
|
|
281
|
+
* @param keys - Account keys to find the private key for the UTXO
|
|
282
|
+
* @param outpoint - The outpoint (txid:vout) of the UTXO to spend
|
|
283
|
+
* @returns Promise resolving to array containing the private key for the UTXO
|
|
284
|
+
* @throws {Error} If the UTXO is not found or the associated key is not in the wallet
|
|
285
|
+
*
|
|
286
|
+
* @example
|
|
287
|
+
* ```typescript
|
|
288
|
+
* const txBuilder = new TransactionBuilder();
|
|
289
|
+
* const keys = await account.getKeys();
|
|
290
|
+
* const privateKeys = await prepareDeleteTransaction(
|
|
291
|
+
* txBuilder,
|
|
292
|
+
* keys,
|
|
293
|
+
* 'txid:0'
|
|
294
|
+
* );
|
|
295
|
+
* ```
|
|
296
|
+
*/
|
|
297
|
+
export async function prepareDeleteTransaction(txBuilder: TransactionBuilder, keys: AccountKeys, outpoint: string): Promise<PrivateKey[]> {
|
|
298
|
+
let utxo = await rostrumProvider.getUtxo(outpoint);
|
|
299
|
+
let address = utxo.addresses[0];
|
|
300
|
+
|
|
301
|
+
txBuilder.from({
|
|
302
|
+
outpoint: outpoint,
|
|
303
|
+
address: address,
|
|
304
|
+
satoshis: utxo.amount
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
let allKeys = keys.receiveKeys.concat(keys.changeKeys);
|
|
308
|
+
let addrKey = allKeys.find(k => k.address === address);
|
|
309
|
+
|
|
310
|
+
if (!addrKey) {
|
|
311
|
+
throw new Error('UTXO associated key not found in the wallet');
|
|
312
|
+
}
|
|
313
|
+
return [addrKey.key.privateKey];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Populate a transaction with token authority inputs for specific permissions
|
|
318
|
+
*
|
|
319
|
+
* This function finds and adds token authority UTXOs that have the required
|
|
320
|
+
* permissions for token operations like minting, melting, or creating subgroups.
|
|
321
|
+
* It automatically handles authority renewal if the authority allows it.
|
|
322
|
+
*
|
|
323
|
+
* @param txBuilder - The transaction builder to populate
|
|
324
|
+
* @param keys - Account keys containing addresses with token authorities
|
|
325
|
+
* @param token - The token ID to find authorities for
|
|
326
|
+
* @param perm - The permission type required ('mint', 'melt', 'subgroup', etc.)
|
|
327
|
+
* @param subgroup - Optional subgroup token ID for subgroup operations
|
|
328
|
+
* @param subgroupAddr - Optional address to receive subgroup authority
|
|
329
|
+
* @returns Promise resolving to array of private keys needed for signing
|
|
330
|
+
* @throws {Error} If the required authority is not found
|
|
331
|
+
*
|
|
332
|
+
* @example
|
|
333
|
+
* ```typescript
|
|
334
|
+
* const txBuilder = new TransactionBuilder();
|
|
335
|
+
* const keys = await account.getKeys();
|
|
336
|
+
* const privateKeys = await populateTokenAuth(
|
|
337
|
+
* txBuilder,
|
|
338
|
+
* keys,
|
|
339
|
+
* 'token_id_hex',
|
|
340
|
+
* 'mint'
|
|
341
|
+
* );
|
|
342
|
+
* ```
|
|
343
|
+
*/
|
|
344
|
+
export async function populateTokenAuth(txBuilder: TransactionBuilder, keys: AccountKeys, token: string, perm: PermissionLabel, subgroup = '', subgroupAddr = ''): Promise<PrivateKey[]> {
|
|
345
|
+
let allKeys = keys.receiveKeys.concat(keys.changeKeys);
|
|
346
|
+
for (let key of allKeys) {
|
|
347
|
+
let utxos = await rostrumProvider.getTokenUtxos(key.address, token);
|
|
348
|
+
for (let utxo of utxos) {
|
|
349
|
+
if (!isAuthFit(utxo.token_amount, perm)) {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
txBuilder.from({
|
|
354
|
+
outpoint: utxo.outpoint_hash,
|
|
355
|
+
address: key.address,
|
|
356
|
+
satoshis: utxo.value
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
if (perm === 'subgroup') {
|
|
360
|
+
txBuilder.to(subgroupAddr, Transaction.DUST_AMOUNT, subgroup, dupAuthority(utxo.token_amount, false));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// if renew flag included, we don't want to burn it
|
|
364
|
+
if (GroupToken.allowsRenew(BigInt.asUintN(64, BigInt(utxo.token_amount)))) {
|
|
365
|
+
txBuilder.to(keys.receiveKeys.at(-1)!.address, Transaction.DUST_AMOUNT, token, dupAuthority(utxo.token_amount));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return [key.key.privateKey];
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
throw new Error("The requested authority not found");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Populate a transaction with multiple token authorities and create duplicates
|
|
377
|
+
*
|
|
378
|
+
* This function finds token authority UTXOs for multiple permissions and
|
|
379
|
+
* creates duplicate outputs for each authority. This is useful for complex
|
|
380
|
+
* token operations that require multiple permissions while preserving the authorities.
|
|
381
|
+
*
|
|
382
|
+
* @param txBuilder - The transaction builder to populate
|
|
383
|
+
* @param keys - Account keys containing addresses with token authorities
|
|
384
|
+
* @param token - The token ID to find authorities for
|
|
385
|
+
* @param perms - Array of permission types required
|
|
386
|
+
* @param toAddr
|
|
387
|
+
* @returns Promise resolving to array of private keys needed for signing
|
|
388
|
+
* @throws {Error} If any required authority is not found
|
|
389
|
+
*
|
|
390
|
+
* @example
|
|
391
|
+
* ```typescript
|
|
392
|
+
* const txBuilder = new TransactionBuilder();
|
|
393
|
+
* const keys = await account.getKeys();
|
|
394
|
+
* const privateKeys = await populateAndDuplicateTokenAuths(
|
|
395
|
+
* txBuilder,
|
|
396
|
+
* keys,
|
|
397
|
+
* 'token_id_hex',
|
|
398
|
+
* ['mint', 'melt']
|
|
399
|
+
* );
|
|
400
|
+
* ```
|
|
401
|
+
*/
|
|
402
|
+
export async function populateAndDuplicateTokenAuths(txBuilder: TransactionBuilder, keys: AccountKeys, token: string, perms: PermissionLabel[], toAddr?: string): Promise<PrivateKey[]> {
|
|
403
|
+
let allKeys = keys.receiveKeys.concat(keys.changeKeys);
|
|
404
|
+
let usedKeys: PrivateKey[] = [];
|
|
405
|
+
|
|
406
|
+
let reqiredPerms = new Set(perms);
|
|
407
|
+
reqiredPerms.add('authorise');
|
|
408
|
+
|
|
409
|
+
for (let key of allKeys) {
|
|
410
|
+
let utxos = await rostrumProvider.getTokenUtxos(key.address, token);
|
|
411
|
+
for (let utxo of utxos) {
|
|
412
|
+
if (utxo.token_amount > 0) {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
let found = false;
|
|
417
|
+
for (let perm of reqiredPerms) {
|
|
418
|
+
if (isAuthFit(utxo.token_amount, perm)) {
|
|
419
|
+
reqiredPerms.delete(perm);
|
|
420
|
+
found = true;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (!found) {
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
txBuilder.from({
|
|
429
|
+
outpoint: utxo.outpoint_hash,
|
|
430
|
+
address: key.address,
|
|
431
|
+
satoshis: utxo.value
|
|
432
|
+
});
|
|
433
|
+
usedKeys.push(key.key.privateKey);
|
|
434
|
+
|
|
435
|
+
// duplicate
|
|
436
|
+
txBuilder.to(toAddr != null ? toAddr : keys.receiveKeys.at(-1)!.address, Transaction.DUST_AMOUNT, token, dupAuthority(utxo.token_amount));
|
|
437
|
+
|
|
438
|
+
if (reqiredPerms.size === 0) {
|
|
439
|
+
return usedKeys;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
throw new Error("The required authorities not found");
|
|
445
|
+
}
|