@sequence0/sdk 2.0.0 → 2.1.0
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/README.md +10 -10
- package/dist/chains/casper.d.ts +74 -0
- package/dist/chains/casper.d.ts.map +1 -0
- package/dist/chains/casper.js +512 -0
- package/dist/chains/casper.js.map +1 -0
- package/dist/chains/cosmos.d.ts +22 -0
- package/dist/chains/cosmos.d.ts.map +1 -1
- package/dist/chains/cosmos.js +113 -12
- package/dist/chains/cosmos.js.map +1 -1
- package/dist/chains/ethereum.d.ts.map +1 -1
- package/dist/chains/ethereum.js +14 -2
- package/dist/chains/ethereum.js.map +1 -1
- package/dist/chains/flow.d.ts +57 -0
- package/dist/chains/flow.d.ts.map +1 -0
- package/dist/chains/flow.js +435 -0
- package/dist/chains/flow.js.map +1 -0
- package/dist/chains/icp.d.ts.map +1 -1
- package/dist/chains/icp.js +483 -67
- package/dist/chains/icp.js.map +1 -1
- package/dist/chains/iota.d.ts +80 -0
- package/dist/chains/iota.d.ts.map +1 -0
- package/dist/chains/iota.js +502 -0
- package/dist/chains/iota.js.map +1 -0
- package/dist/chains/kadena.d.ts +81 -0
- package/dist/chains/kadena.d.ts.map +1 -0
- package/dist/chains/kadena.js +356 -0
- package/dist/chains/kadena.js.map +1 -0
- package/dist/chains/near.d.ts +4 -1
- package/dist/chains/near.d.ts.map +1 -1
- package/dist/chains/near.js +58 -15
- package/dist/chains/near.js.map +1 -1
- package/dist/chains/nervos.d.ts +148 -0
- package/dist/chains/nervos.d.ts.map +1 -0
- package/dist/chains/nervos.js +913 -0
- package/dist/chains/nervos.js.map +1 -0
- package/dist/chains/radix.d.ts +81 -0
- package/dist/chains/radix.d.ts.map +1 -0
- package/dist/chains/radix.js +289 -0
- package/dist/chains/radix.js.map +1 -0
- package/dist/chains/solana.d.ts +4 -0
- package/dist/chains/solana.d.ts.map +1 -1
- package/dist/chains/solana.js +47 -13
- package/dist/chains/solana.js.map +1 -1
- package/dist/chains/stacks.d.ts +113 -0
- package/dist/chains/stacks.d.ts.map +1 -0
- package/dist/chains/stacks.js +576 -0
- package/dist/chains/stacks.js.map +1 -0
- package/dist/chains/sui.d.ts +11 -0
- package/dist/chains/sui.d.ts.map +1 -1
- package/dist/chains/sui.js +49 -8
- package/dist/chains/sui.js.map +1 -1
- package/dist/core/atomic.d.ts +1 -1
- package/dist/core/atomic.js +1 -1
- package/dist/core/client.d.ts +3 -3
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +11 -11
- package/dist/core/client.js.map +1 -1
- package/dist/core/delegation.d.ts +2 -2
- package/dist/core/delegation.d.ts.map +1 -1
- package/dist/core/delegation.js +1 -1
- package/dist/core/programmable.d.ts +1 -1
- package/dist/core/programmable.js +1 -1
- package/dist/core/solvency.d.ts +3 -3
- package/dist/core/solvency.js +3 -3
- package/dist/core/types.d.ts +94 -2
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/universal-account.d.ts +3 -3
- package/dist/core/universal-account.js +3 -3
- package/dist/core/witness.d.ts +3 -3
- package/dist/core/witness.js +3 -3
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/settlement/settlement.d.ts +6 -6
- package/dist/settlement/settlement.d.ts.map +1 -1
- package/dist/settlement/settlement.js +5 -5
- package/dist/utils/discovery.d.ts.map +1 -1
- package/dist/utils/discovery.js +23 -7
- package/dist/utils/discovery.js.map +1 -1
- package/dist/utils/http.d.ts +1 -1
- package/dist/utils/http.d.ts.map +1 -1
- package/dist/utils/http.js +7 -2
- package/dist/utils/http.js.map +1 -1
- package/dist/utils/logger.d.ts +3 -3
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +3 -3
- package/dist/utils/logger.js.map +1 -1
- package/dist/wallet/wallet.d.ts.map +1 -1
- package/dist/wallet/wallet.js +47 -2
- package/dist/wallet/wallet.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stacks (STX) Chain Adapter
|
|
3
|
+
*
|
|
4
|
+
* Builds Stacks SIP-005 token transfer transactions, attaches secp256k1 ECDSA
|
|
5
|
+
* signatures from the FROST threshold signing network, and broadcasts via
|
|
6
|
+
* Stacks node HTTP API.
|
|
7
|
+
*
|
|
8
|
+
* Uses @stacks/transactions if available, falls back to manual serialization
|
|
9
|
+
* using native fetch and Node.js crypto (no external deps required).
|
|
10
|
+
*
|
|
11
|
+
* Stacks uses secp256k1 (same curve as Bitcoin/Ethereum).
|
|
12
|
+
*
|
|
13
|
+
* Optional: npm install @stacks/transactions
|
|
14
|
+
*/
|
|
15
|
+
import { ChainAdapter, StacksTransaction } from '../core/types';
|
|
16
|
+
export declare class StacksAdapter implements ChainAdapter {
|
|
17
|
+
private rpcUrl;
|
|
18
|
+
private network;
|
|
19
|
+
private stacksLib;
|
|
20
|
+
private useNativeLib;
|
|
21
|
+
constructor(network?: 'mainnet' | 'testnet', rpcUrl?: string);
|
|
22
|
+
getRpcUrl(): string;
|
|
23
|
+
/**
|
|
24
|
+
* Build an unsigned Stacks token transfer transaction (SIP-005).
|
|
25
|
+
*
|
|
26
|
+
* The returned hex string encodes a JSON payload containing:
|
|
27
|
+
* - signingHash: SHA-512/256 digest of the transaction with auth cleared
|
|
28
|
+
* - serializedTx: hex-encoded unsigned transaction bytes
|
|
29
|
+
* - nonce, fee, and other metadata needed for signature attachment
|
|
30
|
+
*/
|
|
31
|
+
buildTransaction(tx: StacksTransaction, fromAddress: string): Promise<string>;
|
|
32
|
+
getSigningPayload(unsignedTx: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* Attach a secp256k1 ECDSA signature to the Stacks transaction.
|
|
35
|
+
*
|
|
36
|
+
* Stacks requires a 65-byte recoverable signature in the auth spending condition:
|
|
37
|
+
* [recovery_id (1 byte)] [r (32 bytes)] [s (32 bytes)]
|
|
38
|
+
*
|
|
39
|
+
* The FROST signature is expected as 65 bytes: r (32) + s (32) + v (1),
|
|
40
|
+
* matching the EVM convention. We reorder to Stacks format.
|
|
41
|
+
*/
|
|
42
|
+
attachSignature(unsignedTx: string, signature: string): Promise<string>;
|
|
43
|
+
/**
|
|
44
|
+
* Broadcast a signed Stacks transaction via /v2/transactions
|
|
45
|
+
*/
|
|
46
|
+
broadcast(signedTx: string): Promise<string>;
|
|
47
|
+
/**
|
|
48
|
+
* Get STX balance in microSTX
|
|
49
|
+
*/
|
|
50
|
+
getBalance(address: string): Promise<string>;
|
|
51
|
+
private buildWithLib;
|
|
52
|
+
private attachWithLib;
|
|
53
|
+
private buildManual;
|
|
54
|
+
private attachManual;
|
|
55
|
+
/**
|
|
56
|
+
* Serialize a standard single-sig authorization (P2PKH).
|
|
57
|
+
*
|
|
58
|
+
* Layout:
|
|
59
|
+
* auth_type (1) + hash_mode (1) + signer_hash160 (20) + nonce (8) + fee (8)
|
|
60
|
+
* + key_encoding (1) + signature (65, zeroed for unsigned)
|
|
61
|
+
*/
|
|
62
|
+
private serializeAuth;
|
|
63
|
+
/**
|
|
64
|
+
* Serialize token transfer payload.
|
|
65
|
+
*
|
|
66
|
+
* Layout:
|
|
67
|
+
* payload_type (1) + recipient_principal + amount (8) + memo_length_type (1) + memo (34)
|
|
68
|
+
*/
|
|
69
|
+
private serializeTokenTransferPayload;
|
|
70
|
+
/**
|
|
71
|
+
* Serialize a Stacks principal address.
|
|
72
|
+
*
|
|
73
|
+
* Standard principal: type_id (1) + version (1) + hash160 (20)
|
|
74
|
+
* Contract principal: type_id (1) + version (1) + hash160 (20) + name_length (1) + name
|
|
75
|
+
*/
|
|
76
|
+
private serializePrincipal;
|
|
77
|
+
/**
|
|
78
|
+
* Compute the sighash for a Stacks transaction.
|
|
79
|
+
*
|
|
80
|
+
* The sighash is SHA-512/256 of the serialized transaction with the
|
|
81
|
+
* auth signature field cleared (set to zeros). This is the digest
|
|
82
|
+
* that needs to be signed by the secp256k1 key.
|
|
83
|
+
*
|
|
84
|
+
* Per SIP-005, the initial sighash is computed over the transaction
|
|
85
|
+
* with auth cleared, then wrapped:
|
|
86
|
+
* sighash = SHA-512/256(sighash_presign + auth_type + fee + nonce + key_encoding)
|
|
87
|
+
*
|
|
88
|
+
* For simplicity, we compute the presign sighash (SHA-512/256 of the
|
|
89
|
+
* unsigned serialized tx), which is what the auth spending condition signs.
|
|
90
|
+
*/
|
|
91
|
+
private computeSighash;
|
|
92
|
+
/**
|
|
93
|
+
* Embed a recoverable signature into the serialized transaction.
|
|
94
|
+
*
|
|
95
|
+
* The auth section layout (starting after version + chain_id = offset 5):
|
|
96
|
+
* auth_type (1) + hash_mode (1) + signer_hash160 (20) + nonce (8) + fee (8)
|
|
97
|
+
* + key_encoding (1) + signature (65)
|
|
98
|
+
*
|
|
99
|
+
* Signature starts at offset 5 + 1 + 1 + 20 + 8 + 8 + 1 = offset 44.
|
|
100
|
+
* Stacks signature format: [recovery_id (1)] [r (32)] [s (32)] = 65 bytes.
|
|
101
|
+
*/
|
|
102
|
+
private embedSignatureInSerialized;
|
|
103
|
+
/**
|
|
104
|
+
* Parse a 65-byte signature from FROST (r[32] + s[32] + v[1])
|
|
105
|
+
* into Stacks format (recovery_id, r, s).
|
|
106
|
+
*/
|
|
107
|
+
private parseSignature;
|
|
108
|
+
private fetchNonce;
|
|
109
|
+
private estimateFee;
|
|
110
|
+
}
|
|
111
|
+
export declare function createStacksAdapter(rpcUrl?: string): StacksAdapter;
|
|
112
|
+
export declare function createStacksTestnetAdapter(rpcUrl?: string): StacksAdapter;
|
|
113
|
+
//# sourceMappingURL=stacks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stacks.d.ts","sourceRoot":"","sources":["../../src/chains/stacks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AA+BhE,qBAAa,aAAc,YAAW,YAAY;IAC9C,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,SAAS,CAAM;IACvB,OAAO,CAAC,YAAY,CAAU;gBAElB,OAAO,GAAE,SAAS,GAAG,SAAqB,EAAE,MAAM,CAAC,EAAE,MAAM;IAcvE,SAAS,IAAI,MAAM;IAInB;;;;;;;OAOG;IACG,gBAAgB,CAAC,EAAE,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IA2BnF,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM;IAS7C;;;;;;;;OAQG;IACG,eAAe,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAiB7E;;OAEG;IACG,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IA4BlD;;OAEG;IACG,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;YAiBpC,YAAY;IAyD1B,OAAO,CAAC,aAAa;IAuBrB,OAAO,CAAC,WAAW;IAsDnB,OAAO,CAAC,YAAY;IAmBpB;;;;;;OAMG;IACH,OAAO,CAAC,aAAa;IAarB;;;;;OAKG;IACH,OAAO,CAAC,6BAA6B;IAsBrC;;;;;OAKG;IACH,OAAO,CAAC,kBAAkB;IA2B1B;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,cAAc;IAOtB;;;;;;;;;OASG;IACH,OAAO,CAAC,0BAA0B;IAgBlC;;;OAGG;IACH,OAAO,CAAC,cAAc;YA2BR,UAAU;YASV,WAAW;CAc5B;AAsHD,wBAAgB,mBAAmB,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,aAAa,CAElE;AAED,wBAAgB,0BAA0B,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,aAAa,CAEzE"}
|
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Stacks (STX) Chain Adapter
|
|
4
|
+
*
|
|
5
|
+
* Builds Stacks SIP-005 token transfer transactions, attaches secp256k1 ECDSA
|
|
6
|
+
* signatures from the FROST threshold signing network, and broadcasts via
|
|
7
|
+
* Stacks node HTTP API.
|
|
8
|
+
*
|
|
9
|
+
* Uses @stacks/transactions if available, falls back to manual serialization
|
|
10
|
+
* using native fetch and Node.js crypto (no external deps required).
|
|
11
|
+
*
|
|
12
|
+
* Stacks uses secp256k1 (same curve as Bitcoin/Ethereum).
|
|
13
|
+
*
|
|
14
|
+
* Optional: npm install @stacks/transactions
|
|
15
|
+
*/
|
|
16
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
19
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
20
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
21
|
+
}
|
|
22
|
+
Object.defineProperty(o, k2, desc);
|
|
23
|
+
}) : (function(o, m, k, k2) {
|
|
24
|
+
if (k2 === undefined) k2 = k;
|
|
25
|
+
o[k2] = m[k];
|
|
26
|
+
}));
|
|
27
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
28
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
29
|
+
}) : function(o, v) {
|
|
30
|
+
o["default"] = v;
|
|
31
|
+
});
|
|
32
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
33
|
+
var ownKeys = function(o) {
|
|
34
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
35
|
+
var ar = [];
|
|
36
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
37
|
+
return ar;
|
|
38
|
+
};
|
|
39
|
+
return ownKeys(o);
|
|
40
|
+
};
|
|
41
|
+
return function (mod) {
|
|
42
|
+
if (mod && mod.__esModule) return mod;
|
|
43
|
+
var result = {};
|
|
44
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
45
|
+
__setModuleDefault(result, mod);
|
|
46
|
+
return result;
|
|
47
|
+
};
|
|
48
|
+
})();
|
|
49
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50
|
+
exports.StacksAdapter = void 0;
|
|
51
|
+
exports.createStacksAdapter = createStacksAdapter;
|
|
52
|
+
exports.createStacksTestnetAdapter = createStacksTestnetAdapter;
|
|
53
|
+
const errors_1 = require("../utils/errors");
|
|
54
|
+
const crypto = __importStar(require("crypto"));
|
|
55
|
+
const DEFAULT_RPCS = {
|
|
56
|
+
'mainnet': 'https://stacks-node-api.mainnet.stacks.co',
|
|
57
|
+
'testnet': 'https://stacks-node-api.testnet.stacks.co',
|
|
58
|
+
};
|
|
59
|
+
// Stacks transaction versions (SIP-005)
|
|
60
|
+
const TX_VERSION_MAINNET = 0x00;
|
|
61
|
+
const TX_VERSION_TESTNET = 0x80;
|
|
62
|
+
// Network chain IDs
|
|
63
|
+
const CHAIN_ID_MAINNET = 0x00000001;
|
|
64
|
+
const CHAIN_ID_TESTNET = 0x80000000;
|
|
65
|
+
// Auth types and flags
|
|
66
|
+
const AUTH_TYPE_STANDARD = 0x04;
|
|
67
|
+
const PUBKEY_ENCODING_COMPRESSED = 0x00;
|
|
68
|
+
const SINGLE_SIG_SPENDING_CONDITION = 0x00; // hash mode: P2PKH
|
|
69
|
+
// Payload type for token transfer
|
|
70
|
+
const PAYLOAD_TYPE_TOKEN_TRANSFER = 0x00;
|
|
71
|
+
// Post-condition mode: allow
|
|
72
|
+
const POST_CONDITION_MODE_ALLOW = 0x01;
|
|
73
|
+
// Anchor mode
|
|
74
|
+
const ANCHOR_MODE_ANY = 0x03;
|
|
75
|
+
class StacksAdapter {
|
|
76
|
+
constructor(network = 'mainnet', rpcUrl) {
|
|
77
|
+
this.network = network;
|
|
78
|
+
this.rpcUrl = rpcUrl || DEFAULT_RPCS[network];
|
|
79
|
+
this.useNativeLib = false;
|
|
80
|
+
try {
|
|
81
|
+
this.stacksLib = require('@stacks/transactions');
|
|
82
|
+
this.useNativeLib = true;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// Fall back to manual serialization
|
|
86
|
+
this.stacksLib = null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
getRpcUrl() {
|
|
90
|
+
return this.rpcUrl;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Build an unsigned Stacks token transfer transaction (SIP-005).
|
|
94
|
+
*
|
|
95
|
+
* The returned hex string encodes a JSON payload containing:
|
|
96
|
+
* - signingHash: SHA-512/256 digest of the transaction with auth cleared
|
|
97
|
+
* - serializedTx: hex-encoded unsigned transaction bytes
|
|
98
|
+
* - nonce, fee, and other metadata needed for signature attachment
|
|
99
|
+
*/
|
|
100
|
+
async buildTransaction(tx, fromAddress) {
|
|
101
|
+
try {
|
|
102
|
+
// Fetch nonce if not provided
|
|
103
|
+
const nonce = tx.nonce !== undefined
|
|
104
|
+
? tx.nonce
|
|
105
|
+
: await this.fetchNonce(fromAddress);
|
|
106
|
+
// Fetch fee estimate if not provided
|
|
107
|
+
const fee = tx.fee
|
|
108
|
+
? BigInt(tx.fee)
|
|
109
|
+
: await this.estimateFee();
|
|
110
|
+
const amount = BigInt(tx.amount);
|
|
111
|
+
if (this.useNativeLib) {
|
|
112
|
+
return await this.buildWithLib(tx, fromAddress, nonce, fee, amount);
|
|
113
|
+
}
|
|
114
|
+
return this.buildManual(tx, fromAddress, nonce, fee, amount);
|
|
115
|
+
}
|
|
116
|
+
catch (e) {
|
|
117
|
+
if (e instanceof errors_1.ChainError)
|
|
118
|
+
throw e;
|
|
119
|
+
throw new errors_1.ChainError(`Failed to build Stacks transaction: ${e.message}`, 'stacks');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
getSigningPayload(unsignedTx) {
|
|
123
|
+
try {
|
|
124
|
+
const payload = JSON.parse(Buffer.from(unsignedTx, 'hex').toString());
|
|
125
|
+
return payload.signingHash;
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return unsignedTx;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Attach a secp256k1 ECDSA signature to the Stacks transaction.
|
|
133
|
+
*
|
|
134
|
+
* Stacks requires a 65-byte recoverable signature in the auth spending condition:
|
|
135
|
+
* [recovery_id (1 byte)] [r (32 bytes)] [s (32 bytes)]
|
|
136
|
+
*
|
|
137
|
+
* The FROST signature is expected as 65 bytes: r (32) + s (32) + v (1),
|
|
138
|
+
* matching the EVM convention. We reorder to Stacks format.
|
|
139
|
+
*/
|
|
140
|
+
async attachSignature(unsignedTx, signature) {
|
|
141
|
+
try {
|
|
142
|
+
const payload = JSON.parse(Buffer.from(unsignedTx, 'hex').toString());
|
|
143
|
+
const sig = signature.startsWith('0x') ? signature.slice(2) : signature;
|
|
144
|
+
if (this.useNativeLib) {
|
|
145
|
+
return this.attachWithLib(payload, sig);
|
|
146
|
+
}
|
|
147
|
+
return this.attachManual(payload, sig);
|
|
148
|
+
}
|
|
149
|
+
catch (e) {
|
|
150
|
+
throw new errors_1.ChainError(`Failed to attach Stacks signature: ${e.message}`, 'stacks');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Broadcast a signed Stacks transaction via /v2/transactions
|
|
155
|
+
*/
|
|
156
|
+
async broadcast(signedTx) {
|
|
157
|
+
try {
|
|
158
|
+
const payload = JSON.parse(Buffer.from(signedTx, 'hex').toString());
|
|
159
|
+
const txHex = payload.signedTx || payload.serializedTx;
|
|
160
|
+
const response = await fetch(`${this.rpcUrl}/v2/transactions`, {
|
|
161
|
+
method: 'POST',
|
|
162
|
+
headers: { 'Content-Type': 'application/octet-stream' },
|
|
163
|
+
body: Buffer.from(txHex, 'hex'),
|
|
164
|
+
});
|
|
165
|
+
if (!response.ok) {
|
|
166
|
+
const errorText = await response.text();
|
|
167
|
+
throw new Error(`Broadcast failed (${response.status}): ${errorText}`);
|
|
168
|
+
}
|
|
169
|
+
// The API returns the txid as a JSON string (quoted hex)
|
|
170
|
+
const txId = await response.text();
|
|
171
|
+
return txId.replace(/^"|"$/g, '');
|
|
172
|
+
}
|
|
173
|
+
catch (e) {
|
|
174
|
+
if (e instanceof errors_1.ChainError)
|
|
175
|
+
throw e;
|
|
176
|
+
throw new errors_1.ChainError(`Failed to broadcast Stacks tx: ${e.message}`, 'stacks');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Get STX balance in microSTX
|
|
181
|
+
*/
|
|
182
|
+
async getBalance(address) {
|
|
183
|
+
try {
|
|
184
|
+
const response = await fetch(`${this.rpcUrl}/v2/accounts/${address}?proof=0`);
|
|
185
|
+
if (!response.ok)
|
|
186
|
+
return '0';
|
|
187
|
+
const data = await response.json();
|
|
188
|
+
// balance is returned as hex string prefixed with 0x
|
|
189
|
+
const balanceHex = data.balance;
|
|
190
|
+
return BigInt(balanceHex).toString();
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return '0';
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// ────────────────────────────────────────────────
|
|
197
|
+
// Private: @stacks/transactions library path
|
|
198
|
+
// ────────────────────────────────────────────────
|
|
199
|
+
async buildWithLib(tx, fromAddress, nonce, fee, amount) {
|
|
200
|
+
const { makeUnsignedSTXTokenTransfer, createStacksPrivateKey, serializeTransaction, TransactionVersion, AnchorMode, } = this.stacksLib;
|
|
201
|
+
// Build with a placeholder public key; we only need the structure
|
|
202
|
+
const txOptions = {
|
|
203
|
+
recipient: tx.to,
|
|
204
|
+
amount: amount,
|
|
205
|
+
fee: fee,
|
|
206
|
+
nonce: nonce,
|
|
207
|
+
anchorMode: AnchorMode?.Any ?? 3,
|
|
208
|
+
network: this.network,
|
|
209
|
+
};
|
|
210
|
+
if (tx.memo) {
|
|
211
|
+
txOptions.memo = tx.memo;
|
|
212
|
+
}
|
|
213
|
+
if (tx.senderPublicKey) {
|
|
214
|
+
txOptions.publicKey = tx.senderPublicKey;
|
|
215
|
+
}
|
|
216
|
+
const transaction = await makeUnsignedSTXTokenTransfer(txOptions);
|
|
217
|
+
// Serialize unsigned transaction
|
|
218
|
+
const serialized = Buffer.from(serializeTransaction
|
|
219
|
+
? serializeTransaction(transaction)
|
|
220
|
+
: transaction.serialize());
|
|
221
|
+
// Compute sighash: the digest that needs signing
|
|
222
|
+
// For Stacks, this is SHA-512/256 of the transaction with cleared auth
|
|
223
|
+
const sighash = this.computeSighash(serialized);
|
|
224
|
+
const payload = {
|
|
225
|
+
signingHash: sighash,
|
|
226
|
+
serializedTx: serialized.toString('hex'),
|
|
227
|
+
nonce,
|
|
228
|
+
fee: fee.toString(),
|
|
229
|
+
useLib: true,
|
|
230
|
+
};
|
|
231
|
+
return Buffer.from(JSON.stringify(payload)).toString('hex');
|
|
232
|
+
}
|
|
233
|
+
attachWithLib(payload, sig) {
|
|
234
|
+
// Parse r, s, v from the FROST signature
|
|
235
|
+
const { recoveryId, r, s } = this.parseSignature(sig);
|
|
236
|
+
const serializedTx = Buffer.from(payload.serializedTx, 'hex');
|
|
237
|
+
// The auth field in a single-sig spending condition has a fixed layout.
|
|
238
|
+
// Find the signature slot and write recovery_id + r + s.
|
|
239
|
+
const signedTx = this.embedSignatureInSerialized(serializedTx, recoveryId, r, s);
|
|
240
|
+
const signedPayload = {
|
|
241
|
+
...payload,
|
|
242
|
+
signedTx: signedTx.toString('hex'),
|
|
243
|
+
signature: sig,
|
|
244
|
+
};
|
|
245
|
+
return Buffer.from(JSON.stringify(signedPayload)).toString('hex');
|
|
246
|
+
}
|
|
247
|
+
// ────────────────────────────────────────────────
|
|
248
|
+
// Private: Manual serialization path (no deps)
|
|
249
|
+
// ────────────────────────────────────────────────
|
|
250
|
+
buildManual(tx, _fromAddress, nonce, fee, amount) {
|
|
251
|
+
const isMainnet = this.network === 'mainnet';
|
|
252
|
+
const txVersion = isMainnet ? TX_VERSION_MAINNET : TX_VERSION_TESTNET;
|
|
253
|
+
const chainId = isMainnet ? CHAIN_ID_MAINNET : CHAIN_ID_TESTNET;
|
|
254
|
+
// Sender public key (33 bytes compressed)
|
|
255
|
+
const senderPubKey = tx.senderPublicKey
|
|
256
|
+
? Buffer.from(tx.senderPublicKey, 'hex')
|
|
257
|
+
: Buffer.alloc(33);
|
|
258
|
+
// Hash the public key for the spending condition (Hash160 = RIPEMD160(SHA256(pubkey)))
|
|
259
|
+
const pubkeyHash = hash160(senderPubKey);
|
|
260
|
+
// Build the authorization (standard single-sig, P2PKH)
|
|
261
|
+
const auth = this.serializeAuth(pubkeyHash, nonce, fee);
|
|
262
|
+
// Build the payload (token transfer)
|
|
263
|
+
const tokenPayload = this.serializeTokenTransferPayload(tx.to, amount, tx.memo);
|
|
264
|
+
// Assemble the full transaction:
|
|
265
|
+
// version (1) + chain_id (4) + auth + anchor_mode (1) + post_condition_mode (1)
|
|
266
|
+
// + post_conditions_length (4, 0) + payload
|
|
267
|
+
const parts = [
|
|
268
|
+
Buffer.from([txVersion]),
|
|
269
|
+
writeUint32BE(chainId),
|
|
270
|
+
auth,
|
|
271
|
+
Buffer.from([ANCHOR_MODE_ANY]),
|
|
272
|
+
Buffer.from([POST_CONDITION_MODE_ALLOW]),
|
|
273
|
+
writeUint32BE(0), // post-conditions: empty list (length 0)
|
|
274
|
+
tokenPayload,
|
|
275
|
+
];
|
|
276
|
+
const serialized = Buffer.concat(parts);
|
|
277
|
+
// Compute sighash
|
|
278
|
+
const sighash = this.computeSighash(serialized);
|
|
279
|
+
const payload = {
|
|
280
|
+
signingHash: sighash,
|
|
281
|
+
serializedTx: serialized.toString('hex'),
|
|
282
|
+
nonce,
|
|
283
|
+
fee: fee.toString(),
|
|
284
|
+
useLib: false,
|
|
285
|
+
};
|
|
286
|
+
return Buffer.from(JSON.stringify(payload)).toString('hex');
|
|
287
|
+
}
|
|
288
|
+
attachManual(payload, sig) {
|
|
289
|
+
const { recoveryId, r, s } = this.parseSignature(sig);
|
|
290
|
+
const serializedTx = Buffer.from(payload.serializedTx, 'hex');
|
|
291
|
+
const signedTx = this.embedSignatureInSerialized(serializedTx, recoveryId, r, s);
|
|
292
|
+
const signedPayload = {
|
|
293
|
+
...payload,
|
|
294
|
+
signedTx: signedTx.toString('hex'),
|
|
295
|
+
signature: sig,
|
|
296
|
+
};
|
|
297
|
+
return Buffer.from(JSON.stringify(signedPayload)).toString('hex');
|
|
298
|
+
}
|
|
299
|
+
// ────────────────────────────────────────────────
|
|
300
|
+
// Private: Serialization helpers
|
|
301
|
+
// ────────────────────────────────────────────────
|
|
302
|
+
/**
|
|
303
|
+
* Serialize a standard single-sig authorization (P2PKH).
|
|
304
|
+
*
|
|
305
|
+
* Layout:
|
|
306
|
+
* auth_type (1) + hash_mode (1) + signer_hash160 (20) + nonce (8) + fee (8)
|
|
307
|
+
* + key_encoding (1) + signature (65, zeroed for unsigned)
|
|
308
|
+
*/
|
|
309
|
+
serializeAuth(signerHash160, nonce, fee) {
|
|
310
|
+
const parts = [
|
|
311
|
+
Buffer.from([AUTH_TYPE_STANDARD]),
|
|
312
|
+
Buffer.from([SINGLE_SIG_SPENDING_CONDITION]),
|
|
313
|
+
signerHash160,
|
|
314
|
+
writeUint64BE(BigInt(nonce)),
|
|
315
|
+
writeUint64BE(fee),
|
|
316
|
+
Buffer.from([PUBKEY_ENCODING_COMPRESSED]),
|
|
317
|
+
Buffer.alloc(65), // empty signature placeholder
|
|
318
|
+
];
|
|
319
|
+
return Buffer.concat(parts);
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Serialize token transfer payload.
|
|
323
|
+
*
|
|
324
|
+
* Layout:
|
|
325
|
+
* payload_type (1) + recipient_principal + amount (8) + memo_length_type (1) + memo (34)
|
|
326
|
+
*/
|
|
327
|
+
serializeTokenTransferPayload(recipient, amount, memo) {
|
|
328
|
+
const parts = [
|
|
329
|
+
Buffer.from([PAYLOAD_TYPE_TOKEN_TRANSFER]),
|
|
330
|
+
this.serializePrincipal(recipient),
|
|
331
|
+
writeUint64BE(amount),
|
|
332
|
+
];
|
|
333
|
+
// Memo: always 34 bytes, padded with zeros
|
|
334
|
+
const memoBytes = Buffer.alloc(34);
|
|
335
|
+
if (memo) {
|
|
336
|
+
const encoded = Buffer.from(memo, 'utf-8');
|
|
337
|
+
encoded.copy(memoBytes, 0, 0, Math.min(encoded.length, 34));
|
|
338
|
+
}
|
|
339
|
+
parts.push(memoBytes);
|
|
340
|
+
return Buffer.concat(parts);
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Serialize a Stacks principal address.
|
|
344
|
+
*
|
|
345
|
+
* Standard principal: type_id (1) + version (1) + hash160 (20)
|
|
346
|
+
* Contract principal: type_id (1) + version (1) + hash160 (20) + name_length (1) + name
|
|
347
|
+
*/
|
|
348
|
+
serializePrincipal(address) {
|
|
349
|
+
// Check if this is a contract principal (contains '.')
|
|
350
|
+
const parts = address.split('.');
|
|
351
|
+
// Decode c32check address to get version + hash160
|
|
352
|
+
const { version, hash160: addrHash } = decodec32Address(parts[0]);
|
|
353
|
+
if (parts.length === 2) {
|
|
354
|
+
// Contract principal
|
|
355
|
+
const contractName = Buffer.from(parts[1], 'ascii');
|
|
356
|
+
return Buffer.concat([
|
|
357
|
+
Buffer.from([0x06]), // type: contract principal
|
|
358
|
+
Buffer.from([version]),
|
|
359
|
+
addrHash,
|
|
360
|
+
Buffer.from([contractName.length]),
|
|
361
|
+
contractName,
|
|
362
|
+
]);
|
|
363
|
+
}
|
|
364
|
+
// Standard principal
|
|
365
|
+
return Buffer.concat([
|
|
366
|
+
Buffer.from([0x05]), // type: standard principal
|
|
367
|
+
Buffer.from([version]),
|
|
368
|
+
addrHash,
|
|
369
|
+
]);
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Compute the sighash for a Stacks transaction.
|
|
373
|
+
*
|
|
374
|
+
* The sighash is SHA-512/256 of the serialized transaction with the
|
|
375
|
+
* auth signature field cleared (set to zeros). This is the digest
|
|
376
|
+
* that needs to be signed by the secp256k1 key.
|
|
377
|
+
*
|
|
378
|
+
* Per SIP-005, the initial sighash is computed over the transaction
|
|
379
|
+
* with auth cleared, then wrapped:
|
|
380
|
+
* sighash = SHA-512/256(sighash_presign + auth_type + fee + nonce + key_encoding)
|
|
381
|
+
*
|
|
382
|
+
* For simplicity, we compute the presign sighash (SHA-512/256 of the
|
|
383
|
+
* unsigned serialized tx), which is what the auth spending condition signs.
|
|
384
|
+
*/
|
|
385
|
+
computeSighash(serialized) {
|
|
386
|
+
const hash = crypto.createHash('sha512-256')
|
|
387
|
+
.update(serialized)
|
|
388
|
+
.digest();
|
|
389
|
+
return hash.toString('hex');
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Embed a recoverable signature into the serialized transaction.
|
|
393
|
+
*
|
|
394
|
+
* The auth section layout (starting after version + chain_id = offset 5):
|
|
395
|
+
* auth_type (1) + hash_mode (1) + signer_hash160 (20) + nonce (8) + fee (8)
|
|
396
|
+
* + key_encoding (1) + signature (65)
|
|
397
|
+
*
|
|
398
|
+
* Signature starts at offset 5 + 1 + 1 + 20 + 8 + 8 + 1 = offset 44.
|
|
399
|
+
* Stacks signature format: [recovery_id (1)] [r (32)] [s (32)] = 65 bytes.
|
|
400
|
+
*/
|
|
401
|
+
embedSignatureInSerialized(serialized, recoveryId, r, s) {
|
|
402
|
+
const signed = Buffer.from(serialized);
|
|
403
|
+
const sigOffset = 5 + 1 + 1 + 20 + 8 + 8 + 1; // = 44
|
|
404
|
+
signed[sigOffset] = recoveryId;
|
|
405
|
+
r.copy(signed, sigOffset + 1);
|
|
406
|
+
s.copy(signed, sigOffset + 33);
|
|
407
|
+
return signed;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Parse a 65-byte signature from FROST (r[32] + s[32] + v[1])
|
|
411
|
+
* into Stacks format (recovery_id, r, s).
|
|
412
|
+
*/
|
|
413
|
+
parseSignature(sig) {
|
|
414
|
+
const sigBuf = Buffer.from(sig, 'hex');
|
|
415
|
+
if (sigBuf.length === 65) {
|
|
416
|
+
// Format: r(32) + s(32) + v(1) — EVM convention from FROST
|
|
417
|
+
const r = sigBuf.subarray(0, 32);
|
|
418
|
+
const s = sigBuf.subarray(32, 64);
|
|
419
|
+
let v = sigBuf[64];
|
|
420
|
+
// Normalize v: Ethereum uses 27/28, Stacks uses 0/1
|
|
421
|
+
if (v >= 27)
|
|
422
|
+
v -= 27;
|
|
423
|
+
return { recoveryId: v, r, s };
|
|
424
|
+
}
|
|
425
|
+
else if (sigBuf.length === 64) {
|
|
426
|
+
// Compact format: r(32) + s(32), assume recovery ID 0
|
|
427
|
+
return {
|
|
428
|
+
recoveryId: 0,
|
|
429
|
+
r: sigBuf.subarray(0, 32),
|
|
430
|
+
s: sigBuf.subarray(32, 64),
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
throw new Error(`Unexpected signature length: ${sigBuf.length} bytes`);
|
|
434
|
+
}
|
|
435
|
+
// ────────────────────────────────────────────────
|
|
436
|
+
// Private: RPC helpers
|
|
437
|
+
// ────────────────────────────────────────────────
|
|
438
|
+
async fetchNonce(address) {
|
|
439
|
+
const response = await fetch(`${this.rpcUrl}/v2/accounts/${address}?proof=0`);
|
|
440
|
+
if (!response.ok) {
|
|
441
|
+
throw new Error(`Failed to fetch nonce: ${response.status}`);
|
|
442
|
+
}
|
|
443
|
+
const data = await response.json();
|
|
444
|
+
return data.nonce;
|
|
445
|
+
}
|
|
446
|
+
async estimateFee() {
|
|
447
|
+
try {
|
|
448
|
+
const response = await fetch(`${this.rpcUrl}/v2/fees/transfer`);
|
|
449
|
+
if (!response.ok) {
|
|
450
|
+
// Default fee: 200 microSTX
|
|
451
|
+
return BigInt(200);
|
|
452
|
+
}
|
|
453
|
+
const fee = await response.json();
|
|
454
|
+
// The endpoint returns a number (estimated fee in microSTX)
|
|
455
|
+
return BigInt(fee);
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
return BigInt(200);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
exports.StacksAdapter = StacksAdapter;
|
|
463
|
+
// ────────────────────────────────────────────────────
|
|
464
|
+
// Standalone utility functions
|
|
465
|
+
// ────────────────────────────────────────────────────
|
|
466
|
+
/**
|
|
467
|
+
* Hash160 = RIPEMD-160(SHA-256(data))
|
|
468
|
+
*/
|
|
469
|
+
function hash160(data) {
|
|
470
|
+
const sha256 = crypto.createHash('sha256').update(data).digest();
|
|
471
|
+
return crypto.createHash('ripemd160').update(sha256).digest();
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Write a 32-bit unsigned integer as big-endian.
|
|
475
|
+
*/
|
|
476
|
+
function writeUint32BE(value) {
|
|
477
|
+
const buf = Buffer.alloc(4);
|
|
478
|
+
buf.writeUInt32BE(value, 0);
|
|
479
|
+
return buf;
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Write a 64-bit unsigned integer as big-endian.
|
|
483
|
+
*/
|
|
484
|
+
function writeUint64BE(value) {
|
|
485
|
+
const buf = Buffer.alloc(8);
|
|
486
|
+
buf.writeBigUInt64BE(value, 0);
|
|
487
|
+
return buf;
|
|
488
|
+
}
|
|
489
|
+
// ────────────────────────────────────────────────────
|
|
490
|
+
// c32check address decoding (minimal, no external deps)
|
|
491
|
+
// ────────────────────────────────────────────────────
|
|
492
|
+
const C32_ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
|
|
493
|
+
/**
|
|
494
|
+
* Decode a Stacks c32check address (SP... or ST...) into version + hash160.
|
|
495
|
+
*
|
|
496
|
+
* c32check format: [version_char] [c32-encoded hash160] [4-byte checksum]
|
|
497
|
+
*/
|
|
498
|
+
function decodec32Address(address) {
|
|
499
|
+
// Strip the leading 'S' prefix
|
|
500
|
+
if (!address.startsWith('S')) {
|
|
501
|
+
throw new Error(`Invalid Stacks address: must start with S, got: ${address}`);
|
|
502
|
+
}
|
|
503
|
+
// The version character is the second character
|
|
504
|
+
const versionChar = address[1];
|
|
505
|
+
let version;
|
|
506
|
+
switch (versionChar) {
|
|
507
|
+
case 'P':
|
|
508
|
+
version = 22;
|
|
509
|
+
break; // mainnet single-sig (P2PKH)
|
|
510
|
+
case 'M':
|
|
511
|
+
version = 20;
|
|
512
|
+
break; // mainnet multi-sig
|
|
513
|
+
case 'T':
|
|
514
|
+
version = 26;
|
|
515
|
+
break; // testnet single-sig
|
|
516
|
+
case 'N':
|
|
517
|
+
version = 21;
|
|
518
|
+
break; // testnet multi-sig
|
|
519
|
+
default:
|
|
520
|
+
throw new Error(`Unknown Stacks address version: S${versionChar}`);
|
|
521
|
+
}
|
|
522
|
+
// The rest is c32-encoded: hash160 (20 bytes) + checksum (4 bytes)
|
|
523
|
+
const dataStr = address.slice(2).toUpperCase();
|
|
524
|
+
const decoded = c32decode(dataStr);
|
|
525
|
+
// Last 4 bytes are checksum, first 20 are the hash160
|
|
526
|
+
if (decoded.length < 20) {
|
|
527
|
+
throw new Error(`Invalid Stacks address: decoded data too short`);
|
|
528
|
+
}
|
|
529
|
+
const hash = decoded.subarray(0, 20);
|
|
530
|
+
return { version, hash160: hash };
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Decode a c32-encoded string into bytes.
|
|
534
|
+
*
|
|
535
|
+
* c32 is a base-32 encoding using the alphabet: 0123456789ABCDEFGHJKMNPQRSTVWXYZ
|
|
536
|
+
* (no I, L, O, U to avoid confusion).
|
|
537
|
+
*/
|
|
538
|
+
function c32decode(input) {
|
|
539
|
+
if (input.length === 0)
|
|
540
|
+
return Buffer.alloc(0);
|
|
541
|
+
// Convert c32 characters to 5-bit values
|
|
542
|
+
const values = [];
|
|
543
|
+
for (const ch of input) {
|
|
544
|
+
const idx = C32_ALPHABET.indexOf(ch);
|
|
545
|
+
if (idx === -1) {
|
|
546
|
+
throw new Error(`Invalid c32 character: ${ch}`);
|
|
547
|
+
}
|
|
548
|
+
values.push(idx);
|
|
549
|
+
}
|
|
550
|
+
// Convert from base-32 to bytes using BigInt arithmetic
|
|
551
|
+
let num = BigInt(0);
|
|
552
|
+
for (const v of values) {
|
|
553
|
+
num = num * BigInt(32) + BigInt(v);
|
|
554
|
+
}
|
|
555
|
+
// Convert BigInt to byte array
|
|
556
|
+
const hexStr = num.toString(16);
|
|
557
|
+
const padded = hexStr.length % 2 === 0 ? hexStr : '0' + hexStr;
|
|
558
|
+
const bytes = Buffer.from(padded, 'hex');
|
|
559
|
+
// The result should be 24 bytes (20 hash + 4 checksum) for addresses
|
|
560
|
+
// Pad with leading zeros if needed
|
|
561
|
+
const expectedLen = 24;
|
|
562
|
+
if (bytes.length < expectedLen) {
|
|
563
|
+
return Buffer.concat([Buffer.alloc(expectedLen - bytes.length), bytes]);
|
|
564
|
+
}
|
|
565
|
+
return bytes;
|
|
566
|
+
}
|
|
567
|
+
// ────────────────────────────────────────────────────
|
|
568
|
+
// Factory functions
|
|
569
|
+
// ────────────────────────────────────────────────────
|
|
570
|
+
function createStacksAdapter(rpcUrl) {
|
|
571
|
+
return new StacksAdapter('mainnet', rpcUrl);
|
|
572
|
+
}
|
|
573
|
+
function createStacksTestnetAdapter(rpcUrl) {
|
|
574
|
+
return new StacksAdapter('testnet', rpcUrl);
|
|
575
|
+
}
|
|
576
|
+
//# sourceMappingURL=stacks.js.map
|