@altiuslabs/tx-sdk 0.1.3
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 +105 -0
- package/altius-tx-sdk-0.1.3.tgz +0 -0
- package/package.json +28 -0
- package/src/client.js +263 -0
- package/src/constants.js +131 -0
- package/src/error.js +44 -0
- package/src/index.js +29 -0
- package/src/nonce.js +83 -0
- package/src/rpc.js +155 -0
- package/src/transaction.js +299 -0
- package/src/utils.js +58 -0
- package/src/wallet.js +123 -0
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Altius JavaScript SDK
|
|
2
|
+
|
|
3
|
+
A pure JavaScript SDK for signing and sending Altius USD multi-token transactions. Supports the USD Multi-Token fee model (0x7a transaction type).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @altius/tx-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Quick Start
|
|
14
|
+
|
|
15
|
+
```javascript
|
|
16
|
+
import { TxClient, generate_private_key } from '@altius/tx-sdk';
|
|
17
|
+
import { USDA_ADDRESS } from '@altius/tx-sdk/constants';
|
|
18
|
+
|
|
19
|
+
// Generate a new wallet
|
|
20
|
+
const privateKey = generate_private_key();
|
|
21
|
+
|
|
22
|
+
// Create a client
|
|
23
|
+
const client = new TxClient(privateKey, 'https://rpc.altius.xyz', USDA_ADDRESS);
|
|
24
|
+
|
|
25
|
+
// Transfer USDA
|
|
26
|
+
const txHash = await client.transfer_usda(recipient, 1_000_000); // 1 USD
|
|
27
|
+
console.log('Transaction hash:', txHash);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Configuration
|
|
31
|
+
|
|
32
|
+
```javascript
|
|
33
|
+
import { TxClientBuilder, BASE_FEE_ATTO, DEFAULT_GAS_LIMIT } from '@altius/tx-sdk';
|
|
34
|
+
|
|
35
|
+
const client = new TxClientBuilder(privateKey, 'https://rpc.altius.xyz')
|
|
36
|
+
.fee_token('0xa1700000000000000000000000000000000000001')
|
|
37
|
+
.base_fee(BASE_FEE_ATTO)
|
|
38
|
+
.gas_limit(DEFAULT_GAS_LIMIT)
|
|
39
|
+
.build();
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Transaction Operations
|
|
43
|
+
|
|
44
|
+
```javascript
|
|
45
|
+
// Get account balance
|
|
46
|
+
const balance = await client.fee_token_balance(address);
|
|
47
|
+
|
|
48
|
+
// Send ERC20 transfer
|
|
49
|
+
const { tx_hash } = await client.send_erc20_transfer(
|
|
50
|
+
token_address,
|
|
51
|
+
recipient,
|
|
52
|
+
BigInt(1_000_000_000_000_000000) // 1 token with 18 decimals
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// Send and wait for receipt
|
|
56
|
+
const receipt = await client.send_and_wait(tx, 60000);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## API Reference
|
|
60
|
+
|
|
61
|
+
### Core Classes
|
|
62
|
+
|
|
63
|
+
- **TxClient** - High-level client for sending transactions
|
|
64
|
+
- **Wallet** - Key management and address derivation
|
|
65
|
+
- **RpcClient** - JSON-RPC interaction
|
|
66
|
+
- **NonceManager** - Nonce management
|
|
67
|
+
- **TxBuilder** - Transaction building
|
|
68
|
+
|
|
69
|
+
### Constants
|
|
70
|
+
|
|
71
|
+
```javascript
|
|
72
|
+
import {
|
|
73
|
+
USDA_ADDRESS,
|
|
74
|
+
FEE_TOKEN_FACTORY_ADDRESS,
|
|
75
|
+
FEE_MANAGER_ADDRESS,
|
|
76
|
+
ALT_FEE_TOKEN_ADDRESS,
|
|
77
|
+
BASE_FEE_ATTO,
|
|
78
|
+
DEFAULT_GAS_LIMIT,
|
|
79
|
+
} from '@altius/tx-sdk/constants';
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Security
|
|
83
|
+
|
|
84
|
+
**IMPORTANT**: Private keys never leave the client. All signing happens locally.
|
|
85
|
+
|
|
86
|
+
```javascript
|
|
87
|
+
// WRONG - never send private key to server
|
|
88
|
+
await fetch('/api/send', { body: { private_key: "0x..." } });
|
|
89
|
+
|
|
90
|
+
// CORRECT - sign locally, send raw transaction
|
|
91
|
+
const signedTx = await client.sign(tx);
|
|
92
|
+
await rpcClient.send('eth_sendRawTransaction', [signedTx.raw_transaction]);
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Transaction Type
|
|
96
|
+
|
|
97
|
+
This SDK supports the USD Multi-Token fee model (0x7a transaction type):
|
|
98
|
+
|
|
99
|
+
- **fee_token**: ERC20 token address used for gas payment
|
|
100
|
+
- **fee_payer**: Account paying the fee (optional, defaults to sender)
|
|
101
|
+
- **max_fee_per_gas_usd_attodollars**: Max gas price in USD attodollars/gas
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
MIT
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@altiuslabs/tx-sdk",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "SDK for signing and sending Altius USD multi-token transactions",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js",
|
|
9
|
+
"./constants": "./src/constants.js"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "echo \"No tests yet\" && exit 0"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"altius",
|
|
16
|
+
"ethereum",
|
|
17
|
+
"sdk",
|
|
18
|
+
"constants",
|
|
19
|
+
"signer"
|
|
20
|
+
],
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"js-sha3": "^0.9.3",
|
|
23
|
+
"noble-secp256k1": "^1.2.14",
|
|
24
|
+
"rlp": "^3.0.0"
|
|
25
|
+
},
|
|
26
|
+
"author": "",
|
|
27
|
+
"license": "MIT"
|
|
28
|
+
}
|
package/src/client.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* High-level client for sending Altius transactions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { TxBuilder } from './transaction.js';
|
|
6
|
+
import { Wallet } from './wallet.js';
|
|
7
|
+
import { RpcClient } from './rpc.js';
|
|
8
|
+
import { NonceManager } from './nonce.js';
|
|
9
|
+
import { USDA_ADDRESS, DEFAULT_GAS_LIMIT, BASE_FEE_ATTO } from './constants.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* TxClient - High-level client for sending Altius transactions
|
|
13
|
+
*/
|
|
14
|
+
export class TxClient {
|
|
15
|
+
/**
|
|
16
|
+
* Create a new transaction client
|
|
17
|
+
* @param {string} private_key - Private key as 0x-prefixed hex
|
|
18
|
+
* @param {string} rpc_url - JSON-RPC endpoint URL
|
|
19
|
+
* @param {string} fee_token - Fee token address (0x-prefixed hex)
|
|
20
|
+
*/
|
|
21
|
+
constructor(private_key, rpc_url, fee_token) {
|
|
22
|
+
this.wallet = new Wallet(private_key);
|
|
23
|
+
this.rpc = new RpcClient(rpc_url);
|
|
24
|
+
this.nonce_manager = new NonceManager(this.rpc, this.wallet.address);
|
|
25
|
+
this.fee_token = fee_token;
|
|
26
|
+
this.base_fee = BASE_FEE_ATTO;
|
|
27
|
+
this.gas_limit = DEFAULT_GAS_LIMIT;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get the wallet address
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
get address() {
|
|
35
|
+
return this.wallet.address;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get the chain ID
|
|
40
|
+
* @returns {Promise<number>}
|
|
41
|
+
*/
|
|
42
|
+
async chain_id() {
|
|
43
|
+
return this.rpc.get_chain_id();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get current nonce
|
|
48
|
+
* @returns {Promise<number>}
|
|
49
|
+
*/
|
|
50
|
+
async get_nonce() {
|
|
51
|
+
return this.nonce_manager.get_nonce();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Build an ERC20 transfer transaction
|
|
56
|
+
* @param {string} token - Token address
|
|
57
|
+
* @param {string} recipient - Recipient address
|
|
58
|
+
* @param {BigInt} amount - Amount in wei
|
|
59
|
+
* @returns {Promise<object>}
|
|
60
|
+
*/
|
|
61
|
+
async build_erc20_transfer(token, recipient, amount) {
|
|
62
|
+
const chain_id = await this.chain_id();
|
|
63
|
+
const nonce = await this.nonce_manager.get_and_increment_nonce();
|
|
64
|
+
|
|
65
|
+
return new TxBuilder()
|
|
66
|
+
.chain_id(chain_id)
|
|
67
|
+
.nonce(nonce)
|
|
68
|
+
.gas_limit(this.gas_limit)
|
|
69
|
+
.erc20_transfer(token, recipient, amount)
|
|
70
|
+
.fee_token(this.fee_token)
|
|
71
|
+
.max_fee_per_gas_usd(BigInt(this.base_fee) * 2n)
|
|
72
|
+
.build();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Send an ERC20 transfer transaction
|
|
77
|
+
* @param {string} token - Token address
|
|
78
|
+
* @param {string} recipient - Recipient address
|
|
79
|
+
* @param {BigInt} amount - Amount in wei
|
|
80
|
+
* @returns {Promise<{tx: object, tx_hash: string}>}
|
|
81
|
+
*/
|
|
82
|
+
async send_erc20_transfer(token, recipient, amount) {
|
|
83
|
+
const tx = await this.build_erc20_transfer(token, recipient, amount);
|
|
84
|
+
const signed = await this.sign(tx);
|
|
85
|
+
const tx_hash = await this.rpc.send_raw_transaction(signed.raw_transaction);
|
|
86
|
+
return { tx, tx_hash };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Sign a transaction without sending
|
|
91
|
+
* @param {object} tx - Transaction object
|
|
92
|
+
* @returns {Promise<object>}
|
|
93
|
+
*/
|
|
94
|
+
async sign(tx) {
|
|
95
|
+
const tx_builder = this._tx_to_builder(tx);
|
|
96
|
+
return tx_builder.sign(this.wallet);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Send a pre-built transaction
|
|
101
|
+
* @param {object} tx - Transaction object
|
|
102
|
+
* @returns {Promise<string>} Transaction hash
|
|
103
|
+
*/
|
|
104
|
+
async send(tx) {
|
|
105
|
+
const signed = await this.sign(tx);
|
|
106
|
+
return this.rpc.send_raw_transaction(signed.raw_transaction);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Send a transaction and wait for receipt
|
|
111
|
+
* @param {object} tx - Transaction object
|
|
112
|
+
* @param {number} timeout_ms - Timeout in milliseconds
|
|
113
|
+
* @returns {Promise<object>} Transaction receipt
|
|
114
|
+
*/
|
|
115
|
+
async send_and_wait(tx, timeout_ms = 60000) {
|
|
116
|
+
const tx_hash = await this.send(tx);
|
|
117
|
+
return this.rpc.wait_for_receipt(tx_hash, timeout_ms);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Transfer USDA (fee token)
|
|
122
|
+
* @param {string} recipient - Recipient address
|
|
123
|
+
* @param {number} amount_micro - Amount in microdollars
|
|
124
|
+
* @returns {Promise<string>} Transaction hash
|
|
125
|
+
*/
|
|
126
|
+
async transfer_usda(recipient, amount_micro) {
|
|
127
|
+
const { tx_hash } = await this.send_erc20_transfer(
|
|
128
|
+
this.fee_token,
|
|
129
|
+
recipient,
|
|
130
|
+
BigInt(amount_micro)
|
|
131
|
+
);
|
|
132
|
+
return tx_hash;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Transfer USDA and wait for confirmation
|
|
137
|
+
* @param {string} recipient - Recipient address
|
|
138
|
+
* @param {number} amount_micro - Amount in microdollars
|
|
139
|
+
* @param {number} timeout_ms - Timeout in milliseconds
|
|
140
|
+
* @returns {Promise<object>} Transaction receipt
|
|
141
|
+
*/
|
|
142
|
+
async transfer_usda_and_wait(recipient, amount_micro, timeout_ms = 60000) {
|
|
143
|
+
const tx = await this.build_erc20_transfer(
|
|
144
|
+
this.fee_token,
|
|
145
|
+
recipient,
|
|
146
|
+
BigInt(amount_micro)
|
|
147
|
+
);
|
|
148
|
+
return this.send_and_wait(tx, timeout_ms);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get fee token balance for an address
|
|
153
|
+
* @param {string} address - Address to check
|
|
154
|
+
* @returns {Promise<bigint>}
|
|
155
|
+
*/
|
|
156
|
+
async fee_token_balance(address) {
|
|
157
|
+
return this.rpc.get_erc20_balance(this.fee_token, address);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Get native balance for an address
|
|
162
|
+
* @param {string} address - Address to check
|
|
163
|
+
* @returns {Promise<bigint>}
|
|
164
|
+
*/
|
|
165
|
+
async native_balance(address) {
|
|
166
|
+
return this.rpc.get_balance(address);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Reset the nonce cache (useful after errors)
|
|
171
|
+
*/
|
|
172
|
+
reset_nonce() {
|
|
173
|
+
this.nonce_manager.reset_nonce();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Convert tx object back to builder for signing
|
|
178
|
+
* @private
|
|
179
|
+
*/
|
|
180
|
+
_tx_to_builder(tx) {
|
|
181
|
+
const builder = new TxBuilder()
|
|
182
|
+
.chain_id(tx.chain_id)
|
|
183
|
+
.nonce(tx.nonce)
|
|
184
|
+
.gas_limit(tx.gas_limit)
|
|
185
|
+
.to(tx.to)
|
|
186
|
+
.value(tx.value)
|
|
187
|
+
.data(tx.data)
|
|
188
|
+
.fee_token(tx.fee_token)
|
|
189
|
+
.fee_payer(tx.fee_payer)
|
|
190
|
+
.fee_payer_signature(tx.fee_payer_signature);
|
|
191
|
+
|
|
192
|
+
if (tx.max_fee_per_gas_usd_attodollars) {
|
|
193
|
+
builder.max_fee_per_gas_usd(tx.max_fee_per_gas_usd_attodollars);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return builder;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* TxClientBuilder - Builder for TxClient
|
|
202
|
+
*/
|
|
203
|
+
export class TxClientBuilder {
|
|
204
|
+
constructor(private_key, rpc_url) {
|
|
205
|
+
this.private_key = private_key;
|
|
206
|
+
this.rpc_url = rpc_url;
|
|
207
|
+
this.fee_token = USDA_ADDRESS;
|
|
208
|
+
this.base_fee = BASE_FEE_ATTO;
|
|
209
|
+
this.gas_limit = DEFAULT_GAS_LIMIT;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Set fee token address
|
|
214
|
+
* @param {string} fee_token
|
|
215
|
+
* @returns {TxClientBuilder}
|
|
216
|
+
*/
|
|
217
|
+
fee_token(fee_token) {
|
|
218
|
+
this.fee_token = fee_token;
|
|
219
|
+
return this;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Set base fee in attodollars
|
|
224
|
+
* @param {number} base_fee
|
|
225
|
+
* @returns {TxClientBuilder}
|
|
226
|
+
*/
|
|
227
|
+
base_fee(base_fee) {
|
|
228
|
+
this.base_fee = base_fee;
|
|
229
|
+
return this;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Set gas limit
|
|
234
|
+
* @param {number} gas_limit
|
|
235
|
+
* @returns {TxClientBuilder}
|
|
236
|
+
*/
|
|
237
|
+
gas_limit(gas_limit) {
|
|
238
|
+
this.gas_limit = gas_limit;
|
|
239
|
+
return this;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Build the TxClient
|
|
244
|
+
* @returns {TxClient}
|
|
245
|
+
*/
|
|
246
|
+
build() {
|
|
247
|
+
const client = new TxClient(this.private_key, this.rpc_url, this.fee_token);
|
|
248
|
+
client.base_fee = this.base_fee;
|
|
249
|
+
client.gas_limit = this.gas_limit;
|
|
250
|
+
return client;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Create a new TxClient
|
|
256
|
+
* @param {string} private_key - Private key
|
|
257
|
+
* @param {string} rpc_url - RPC URL
|
|
258
|
+
* @param {string} fee_token - Fee token address
|
|
259
|
+
* @returns {TxClient}
|
|
260
|
+
*/
|
|
261
|
+
export function create_tx_client(private_key, rpc_url, fee_token) {
|
|
262
|
+
return new TxClient(private_key, rpc_url, fee_token);
|
|
263
|
+
}
|
package/src/constants.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Altius SDK - JavaScript/TypeScript Constants
|
|
3
|
+
*
|
|
4
|
+
* This is the single source of truth for all address and configuration values
|
|
5
|
+
* used by:
|
|
6
|
+
* - Frontend (TypeScript)
|
|
7
|
+
* - Tests (JavaScript/Node.js)
|
|
8
|
+
* - Backend (via altius-tx-sdk)
|
|
9
|
+
*
|
|
10
|
+
* IMPORTANT: When modifying addresses or constants, update this file first,
|
|
11
|
+
* then propagate changes to other SDKs.
|
|
12
|
+
*
|
|
13
|
+
* This file is generated from altius-predeploy/src/addresses.rs
|
|
14
|
+
* Do not edit manually - update the source and regenerate.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Address Constants
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* USDA Fee Token (ERC20) - Genesis Fee Token
|
|
23
|
+
* Address: 0xa1700000000000000000000000000000000000001 (index 1)
|
|
24
|
+
*/
|
|
25
|
+
export const USDA_ADDRESS = '0xa1700000000000000000000000000000000000001';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Fee Token Factory
|
|
29
|
+
* Address: 0xa1700000000000000000000000000000000000000 (index 0)
|
|
30
|
+
*/
|
|
31
|
+
export const FEE_TOKEN_FACTORY_ADDRESS = '0xa1700000000000000000000000000000000000000';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Fee Manager (Independent prefix)
|
|
35
|
+
* Address: 0xFE0000000000000000000000000000000000000001
|
|
36
|
+
*/
|
|
37
|
+
export const FEE_MANAGER_ADDRESS = '0xFE0000000000000000000000000000000000000001';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Alternative fee token for testing (index 2)
|
|
41
|
+
*/
|
|
42
|
+
export const ALT_FEE_TOKEN_ADDRESS = '0xa170000000000000000000000000000000000002';
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Fee token prefix (10 bytes) for validation
|
|
46
|
+
*/
|
|
47
|
+
export const FEE_TOKEN_PREFIX = '0xa1700000';
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Zero address
|
|
51
|
+
*/
|
|
52
|
+
export const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';
|
|
53
|
+
|
|
54
|
+
// Valid addresses for testing
|
|
55
|
+
export const VALID_ADDRESSES = {
|
|
56
|
+
USDA: USDA_ADDRESS,
|
|
57
|
+
FACTORY: FEE_TOKEN_FACTORY_ADDRESS,
|
|
58
|
+
ALT_TOKEN: ALT_FEE_TOKEN_ADDRESS,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Invalid addresses for testing
|
|
62
|
+
export const INVALID_ADDRESSES = {
|
|
63
|
+
NO_PREFIX: '0x1234567890123456789012345678901234567890',
|
|
64
|
+
WETH: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
|
|
65
|
+
ZERO: ZERO_ADDRESS,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// ============================================================================
|
|
69
|
+
// Fee Configuration
|
|
70
|
+
// ============================================================================
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Base fee in attodollars per gas (50 G-attodollars/gas)
|
|
74
|
+
*/
|
|
75
|
+
export const BASE_FEE_ATTO = 50_000_000_000;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Default max fee per gas (2x base fee)
|
|
79
|
+
*/
|
|
80
|
+
export const DEFAULT_MAX_FEE_PER_GAS = 100_000_000_000;
|
|
81
|
+
|
|
82
|
+
// ============================================================================
|
|
83
|
+
// Token Configuration
|
|
84
|
+
// ============================================================================
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Default faucet amount in microdollars (100 USDA)
|
|
88
|
+
*/
|
|
89
|
+
export const FAUCET_AMOUNT_MICRO = BigInt(100_000_000);
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Default faucet amount in token units (100 USDA with 18 decimals)
|
|
93
|
+
*/
|
|
94
|
+
export const FAUCET_AMOUNT_WEI = BigInt(100_000_000_000_000_000_000);
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Default transfer amount in microdollars (10 USDA)
|
|
98
|
+
*/
|
|
99
|
+
export const TRANSFER_AMOUNT_MICRO = BigInt(10_000_000);
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Default transfer amount in token units (10 USDA with 18 decimals)
|
|
103
|
+
*/
|
|
104
|
+
export const TRANSFER_AMOUNT_WEI = BigInt(10_000_000_000_000_000_000);
|
|
105
|
+
|
|
106
|
+
// ============================================================================
|
|
107
|
+
// Gas Configuration
|
|
108
|
+
// ============================================================================
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Default gas limit for ERC20 transfers
|
|
112
|
+
*/
|
|
113
|
+
export const DEFAULT_GAS_LIMIT = 100_000;
|
|
114
|
+
|
|
115
|
+
// ============================================================================
|
|
116
|
+
// Validation Constants
|
|
117
|
+
// ============================================================================
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Reserved address threshold (indices 0-255 are reserved)
|
|
121
|
+
*/
|
|
122
|
+
export const RESERVED_THRESHOLD = 256;
|
|
123
|
+
|
|
124
|
+
// ============================================================================
|
|
125
|
+
// Faucet Configuration
|
|
126
|
+
// ============================================================================
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Default fee token for faucet (USDA)
|
|
130
|
+
*/
|
|
131
|
+
export const FAUCET_FEE_TOKEN = USDA_ADDRESS;
|
package/src/error.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error types for Altius SDK
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export class AltiusError extends Error {
|
|
6
|
+
constructor(message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'AltiusError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class InvalidPrivateKeyError extends AltiusError {
|
|
13
|
+
constructor(message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = 'InvalidPrivateKeyError';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class SigningError extends AltiusError {
|
|
20
|
+
constructor(message) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = 'SigningError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class RpcError extends AltiusError {
|
|
27
|
+
constructor(message, code = -1) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.name = 'RpcError';
|
|
30
|
+
this.code = code;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class MissingFieldError extends AltiusError {
|
|
35
|
+
constructor(field) {
|
|
36
|
+
super(`${field} is required`);
|
|
37
|
+
this.name = 'MissingFieldError';
|
|
38
|
+
this.field = field;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Re-export error classes for convenience
|
|
43
|
+
export const Error = AltiusError;
|
|
44
|
+
export const Result = AltiusError;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Altius JavaScript SDK
|
|
3
|
+
*
|
|
4
|
+
* A pure JavaScript SDK for signing and sending Altius transactions.
|
|
5
|
+
* Supports the USD Multi-Token fee model (0x7a transaction type).
|
|
6
|
+
*
|
|
7
|
+
* IMPORTANT: Private keys never leave the client. All signing happens locally.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Error types
|
|
11
|
+
export * from './error.js';
|
|
12
|
+
|
|
13
|
+
// Wallet and signing
|
|
14
|
+
export { Wallet, generate_private_key, private_key_to_address, sign_hash } from './wallet.js';
|
|
15
|
+
|
|
16
|
+
// Transaction building
|
|
17
|
+
export { TxBuilder, create_transaction, fee_payer_signature_hash } from './transaction.js';
|
|
18
|
+
|
|
19
|
+
// RPC client
|
|
20
|
+
export { RpcClient, create_rpc_client } from './rpc.js';
|
|
21
|
+
|
|
22
|
+
// Nonce management
|
|
23
|
+
export { NonceManager, create_nonce_manager } from './nonce.js';
|
|
24
|
+
|
|
25
|
+
// High-level client
|
|
26
|
+
export { TxClient, TxClientBuilder, create_tx_client } from './client.js';
|
|
27
|
+
|
|
28
|
+
// Constants
|
|
29
|
+
export * from './constants.js';
|
package/src/nonce.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nonce manager for tracking and managing transaction nonces
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { RpcClient } from './rpc.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* NonceManager - manages transaction nonces for an address
|
|
9
|
+
*/
|
|
10
|
+
export class NonceManager {
|
|
11
|
+
/**
|
|
12
|
+
* Create a new NonceManager
|
|
13
|
+
* @param {RpcClient} rpcClient - RPC client instance
|
|
14
|
+
* @param {string} address - Address to manage nonces for (0x-prefixed hex)
|
|
15
|
+
*/
|
|
16
|
+
constructor(rpcClient, address) {
|
|
17
|
+
this.rpcClient = rpcClient;
|
|
18
|
+
this.address = address;
|
|
19
|
+
this.localNonce = null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get the current nonce (from local cache or RPC)
|
|
24
|
+
* @returns {Promise<number>}
|
|
25
|
+
*/
|
|
26
|
+
async get_nonce() {
|
|
27
|
+
if (this.localNonce !== null) {
|
|
28
|
+
return this.localNonce;
|
|
29
|
+
}
|
|
30
|
+
const nonce = await this.rpcClient.get_nonce(this.address);
|
|
31
|
+
this.localNonce = Number(nonce);
|
|
32
|
+
return this.localNonce;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get and increment the nonce (atomic operation)
|
|
37
|
+
* @returns {Promise<number>}
|
|
38
|
+
*/
|
|
39
|
+
async get_and_increment_nonce() {
|
|
40
|
+
const nonce = await this.get_nonce();
|
|
41
|
+
this.localNonce = nonce + 1;
|
|
42
|
+
return nonce;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Reset the local nonce cache
|
|
47
|
+
*/
|
|
48
|
+
async reset_nonce() {
|
|
49
|
+
this.localNonce = null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Sync local nonce with on-chain state
|
|
54
|
+
* @returns {Promise<number>}
|
|
55
|
+
*/
|
|
56
|
+
async sync_nonce() {
|
|
57
|
+
this.localNonce = null;
|
|
58
|
+
return this.get_nonce();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Advance nonce by a delta (for batch transactions)
|
|
63
|
+
* @param {number} delta - Amount to advance
|
|
64
|
+
* @returns {number}
|
|
65
|
+
*/
|
|
66
|
+
advance_nonce(delta = 1) {
|
|
67
|
+
if (this.localNonce === null) {
|
|
68
|
+
throw new Error('Cannot advance nonce without first calling get_nonce()');
|
|
69
|
+
}
|
|
70
|
+
this.localNonce += delta;
|
|
71
|
+
return this.localNonce - delta;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Create a new NonceManager
|
|
77
|
+
* @param {RpcClient} rpcClient - RPC client instance
|
|
78
|
+
* @param {string} address - Address to manage nonces for
|
|
79
|
+
* @returns {NonceManager}
|
|
80
|
+
*/
|
|
81
|
+
export function create_nonce_manager(rpcClient, address) {
|
|
82
|
+
return new NonceManager(rpcClient, address);
|
|
83
|
+
}
|
package/src/rpc.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RPC Client for Altius
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { numToHex, padHex } from './utils.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* JSON-RPC client for communicating with Altius node
|
|
9
|
+
*/
|
|
10
|
+
export class RpcClient {
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} url - RPC URL (e.g., http://localhost:8545)
|
|
13
|
+
*/
|
|
14
|
+
constructor(url) {
|
|
15
|
+
this.url = url;
|
|
16
|
+
this.id = 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Make a JSON-RPC request
|
|
21
|
+
* @param {string} method - RPC method name
|
|
22
|
+
* @param {Array} params - Method parameters
|
|
23
|
+
* @returns {Promise<any>}
|
|
24
|
+
*/
|
|
25
|
+
async request(method, params = []) {
|
|
26
|
+
const response = await fetch(this.url, {
|
|
27
|
+
method: 'POST',
|
|
28
|
+
headers: {
|
|
29
|
+
'Content-Type': 'application/json',
|
|
30
|
+
},
|
|
31
|
+
body: JSON.stringify({
|
|
32
|
+
jsonrpc: '2.0',
|
|
33
|
+
id: ++this.id,
|
|
34
|
+
method,
|
|
35
|
+
params,
|
|
36
|
+
}),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const result = await response.json();
|
|
40
|
+
|
|
41
|
+
if (result.error) {
|
|
42
|
+
throw new Error(`RPC Error: ${result.error.message}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return result.result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get the chain ID
|
|
50
|
+
* @returns {Promise<number>}
|
|
51
|
+
*/
|
|
52
|
+
async get_chain_id() {
|
|
53
|
+
const hex = await this.request('eth_chainId');
|
|
54
|
+
return parseInt(hex, 16);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get transaction count (nonce) for an address
|
|
59
|
+
* @param {string} address - Address as 0x-prefixed hex
|
|
60
|
+
* @returns {Promise<number>}
|
|
61
|
+
*/
|
|
62
|
+
async get_nonce(address) {
|
|
63
|
+
const hex = await this.request('eth_getTransactionCount', [address, 'pending']);
|
|
64
|
+
return parseInt(hex, 16);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get balance for an address
|
|
69
|
+
* @param {string} address - Address as 0x-prefixed hex
|
|
70
|
+
* @returns {Promise<bigint>}
|
|
71
|
+
*/
|
|
72
|
+
async get_balance(address) {
|
|
73
|
+
const hex = await this.request('eth_getBalance', [address, 'latest']);
|
|
74
|
+
return BigInt(hex);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get code at an address
|
|
79
|
+
* @param {string} address - Address as 0x-prefixed hex
|
|
80
|
+
* @returns {Promise<string>}
|
|
81
|
+
*/
|
|
82
|
+
async get_code(address) {
|
|
83
|
+
return this.request('eth_getCode', [address, 'latest']);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Call a contract (read-only)
|
|
88
|
+
* @param {object} call - Call object with to and data
|
|
89
|
+
* @returns {Promise<string>}
|
|
90
|
+
*/
|
|
91
|
+
async call(call) {
|
|
92
|
+
return this.request('eth_call', [call, 'latest']);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Send a signed raw transaction
|
|
97
|
+
* @param {string} raw_transaction - Signed transaction as 0x-prefixed hex
|
|
98
|
+
* @returns {Promise<string>} Transaction hash
|
|
99
|
+
*/
|
|
100
|
+
async send_raw_transaction(raw_transaction) {
|
|
101
|
+
return this.request('eth_sendRawTransaction', [raw_transaction]);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get transaction receipt
|
|
106
|
+
* @param {string} tx_hash - Transaction hash
|
|
107
|
+
* @returns {Promise<object|null>}
|
|
108
|
+
*/
|
|
109
|
+
async get_transaction_receipt(tx_hash) {
|
|
110
|
+
return this.request('eth_getTransactionReceipt', [tx_hash]);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Wait for transaction to be mined
|
|
115
|
+
* @param {string} tx_hash - Transaction hash
|
|
116
|
+
* @param {number} timeout_ms - Timeout in milliseconds
|
|
117
|
+
* @param {number} poll_interval_ms - Poll interval in milliseconds
|
|
118
|
+
* @returns {Promise<object>} Transaction receipt
|
|
119
|
+
*/
|
|
120
|
+
async wait_for_receipt(tx_hash, timeout_ms = 60000, poll_interval_ms = 1000) {
|
|
121
|
+
const start = Date.now();
|
|
122
|
+
|
|
123
|
+
while (Date.now() - start < timeout_ms) {
|
|
124
|
+
const receipt = await this.get_transaction_receipt(tx_hash);
|
|
125
|
+
if (receipt) {
|
|
126
|
+
return receipt;
|
|
127
|
+
}
|
|
128
|
+
await new Promise(resolve => setTimeout(resolve, poll_interval_ms));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
throw new Error(`Transaction ${tx_hash} timed out after ${timeout_ms}ms`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get ERC20 balance
|
|
136
|
+
* @param {string} token_address - Token contract address
|
|
137
|
+
* @param {string} owner_address - Owner address
|
|
138
|
+
* @returns {Promise<bigint>}
|
|
139
|
+
*/
|
|
140
|
+
async get_erc20_balance(token_address, owner_address) {
|
|
141
|
+
// ERC20 balanceOf selector: 0x70a08231
|
|
142
|
+
const data = '0x70a08231' + padHex(owner_address, 32).slice(2);
|
|
143
|
+
const result = await this.call({ to: token_address, data });
|
|
144
|
+
return BigInt(result);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Create an RPC client
|
|
150
|
+
* @param {string} url - RPC URL
|
|
151
|
+
* @returns {RpcClient}
|
|
152
|
+
*/
|
|
153
|
+
export function create_rpc_client(url) {
|
|
154
|
+
return new RpcClient(url);
|
|
155
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transaction builder for Altius USD Multi-Token transactions (0x7a)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { keccak256, pad_hex, num_to_hex, rlp_encode } from './utils.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Magic byte for fee payer signature (0x7B)
|
|
9
|
+
*/
|
|
10
|
+
const FEE_PAYER_SIGNATURE_MAGIC_BYTE = '0x7b';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Build a USD Multi-Token transaction (0x7a type)
|
|
14
|
+
*/
|
|
15
|
+
export class TxBuilder {
|
|
16
|
+
constructor() {
|
|
17
|
+
this._chain_id = null;
|
|
18
|
+
this._nonce = null;
|
|
19
|
+
this._gas_limit = 21000;
|
|
20
|
+
this._to = null;
|
|
21
|
+
this._value = 0;
|
|
22
|
+
this._data = '0x';
|
|
23
|
+
this._max_priority_fee_per_gas = 0;
|
|
24
|
+
this._max_fee_per_gas = 0;
|
|
25
|
+
this._max_fee_per_gas_usd_attodollars = 0;
|
|
26
|
+
this._fee_token = null;
|
|
27
|
+
this._fee_payer = '0x0000000000000000000000000000000000000000';
|
|
28
|
+
this._fee_payer_signature = '0x';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Set chain ID
|
|
33
|
+
* @param {number} chain_id
|
|
34
|
+
* @returns {TxBuilder}
|
|
35
|
+
*/
|
|
36
|
+
chain_id(chain_id) {
|
|
37
|
+
this._chain_id = chain_id;
|
|
38
|
+
return this;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Set nonce
|
|
43
|
+
* @param {number} nonce
|
|
44
|
+
* @returns {TxBuilder}
|
|
45
|
+
*/
|
|
46
|
+
nonce(nonce) {
|
|
47
|
+
this._nonce = nonce;
|
|
48
|
+
return this;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Set gas limit
|
|
53
|
+
* @param {number} gas_limit
|
|
54
|
+
* @returns {TxBuilder}
|
|
55
|
+
*/
|
|
56
|
+
gas_limit(gas_limit) {
|
|
57
|
+
this._gas_limit = gas_limit;
|
|
58
|
+
return this;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Set recipient
|
|
63
|
+
* @param {string} address - Address as 0x-prefixed hex
|
|
64
|
+
* @returns {TxBuilder}
|
|
65
|
+
*/
|
|
66
|
+
to(address) {
|
|
67
|
+
this._to = address;
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Set value (in wei)
|
|
73
|
+
* @param {BigInt|number} value
|
|
74
|
+
* @returns {TxBuilder}
|
|
75
|
+
*/
|
|
76
|
+
value(value) {
|
|
77
|
+
this._value = BigInt(value);
|
|
78
|
+
return this;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Set input data (calldata)
|
|
83
|
+
* @param {string} data - Calldata as 0x-prefixed hex
|
|
84
|
+
* @returns {TxBuilder}
|
|
85
|
+
*/
|
|
86
|
+
data(data) {
|
|
87
|
+
this._data = data;
|
|
88
|
+
return this;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Set max priority fee per gas (in wei)
|
|
93
|
+
* @param {BigInt|number} max_priority_fee_per_gas
|
|
94
|
+
* @returns {TxBuilder}
|
|
95
|
+
*/
|
|
96
|
+
max_priority_fee_per_gas(max_priority_fee_per_gas) {
|
|
97
|
+
this._max_priority_fee_per_gas = BigInt(max_priority_fee_per_gas);
|
|
98
|
+
return this;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Set max fee per gas (in wei)
|
|
103
|
+
* @param {BigInt|number} max_fee_per_gas
|
|
104
|
+
* @returns {TxBuilder}
|
|
105
|
+
*/
|
|
106
|
+
max_fee_per_gas(max_fee_per_gas) {
|
|
107
|
+
this._max_fee_per_gas = BigInt(max_fee_per_gas);
|
|
108
|
+
return this;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Set max fee per gas in USD attodollars
|
|
113
|
+
* @param {BigInt|number} max_fee_per_gas_usd_attodollars
|
|
114
|
+
* @returns {TxBuilder}
|
|
115
|
+
*/
|
|
116
|
+
max_fee_per_gas_usd(max_fee_per_gas_usd_attodollars) {
|
|
117
|
+
this._max_fee_per_gas_usd_attodollars = BigInt(max_fee_per_gas_usd_attodollars);
|
|
118
|
+
return this;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Set fee token address
|
|
123
|
+
* @param {string} address - Fee token as 0x-prefixed hex
|
|
124
|
+
* @returns {TxBuilder}
|
|
125
|
+
*/
|
|
126
|
+
fee_token(address) {
|
|
127
|
+
this._fee_token = address;
|
|
128
|
+
return this;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Set fee payer address (0-indexed for sender-pays)
|
|
133
|
+
* @param {string} address - Fee payer as 0x-prefixed hex
|
|
134
|
+
* @returns {TxBuilder}
|
|
135
|
+
*/
|
|
136
|
+
fee_payer(address) {
|
|
137
|
+
this._fee_payer = address;
|
|
138
|
+
return this;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Set fee payer signature
|
|
143
|
+
* @param {string} signature - Signature as 0x-prefixed hex
|
|
144
|
+
* @returns {TxBuilder}
|
|
145
|
+
*/
|
|
146
|
+
fee_payer_signature(signature) {
|
|
147
|
+
this._fee_payer_signature = signature;
|
|
148
|
+
return this;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Build the transaction object
|
|
153
|
+
* @returns {object}
|
|
154
|
+
*/
|
|
155
|
+
build() {
|
|
156
|
+
if (!this._chain_id) throw new Error('chain_id is required');
|
|
157
|
+
if (this._nonce === null) throw new Error('nonce is required');
|
|
158
|
+
if (!this._fee_token) throw new Error('fee_token is required');
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
chain_id: this._chain_id,
|
|
162
|
+
nonce: this._nonce,
|
|
163
|
+
gas_limit: this._gas_limit,
|
|
164
|
+
to: this._to || '0x0000000000000000000000000000000000000000',
|
|
165
|
+
value: this._value,
|
|
166
|
+
data: this._data,
|
|
167
|
+
max_priority_fee_per_gas: this._max_priority_fee_per_gas,
|
|
168
|
+
max_fee_per_gas: this._max_fee_per_gas,
|
|
169
|
+
max_fee_per_gas_usd_attodollars: this._max_fee_per_gas_usd_attodollars,
|
|
170
|
+
fee_token: this._fee_token,
|
|
171
|
+
fee_payer: this._fee_payer,
|
|
172
|
+
fee_payer_signature: this._fee_payer_signature,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Compute the sender signature hash for this transaction
|
|
178
|
+
* @returns {string}
|
|
179
|
+
*/
|
|
180
|
+
signature_hash() {
|
|
181
|
+
const tx = this.build();
|
|
182
|
+
|
|
183
|
+
// For 0x7a transaction, the items are:
|
|
184
|
+
// [chainId, nonce, maxPriorityFeePerGas, maxFeePerGas, gasLimit, to, value, data, accessList, feeToken, feePayer, maxFeePerGasUsdAttodollars, feePayerSignature]
|
|
185
|
+
const list = [
|
|
186
|
+
num_to_hex(tx.chain_id, 32),
|
|
187
|
+
num_to_hex(tx.nonce, 32),
|
|
188
|
+
num_to_hex(tx.max_priority_fee_per_gas, 32),
|
|
189
|
+
num_to_hex(tx.max_fee_per_gas, 32),
|
|
190
|
+
num_to_hex(tx.gas_limit, 32),
|
|
191
|
+
tx.to,
|
|
192
|
+
num_to_hex(tx.value, 32),
|
|
193
|
+
tx.data,
|
|
194
|
+
// Empty access list
|
|
195
|
+
'0xc0',
|
|
196
|
+
tx.fee_token,
|
|
197
|
+
tx.fee_payer,
|
|
198
|
+
num_to_hex(tx.max_fee_per_gas_usd_attodollars, 32),
|
|
199
|
+
tx.fee_payer_signature,
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
const encoded = rlp_encode(list);
|
|
203
|
+
return keccak256(encoded);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Sign this transaction with a wallet
|
|
208
|
+
* @param {Wallet} wallet
|
|
209
|
+
* @returns {object} Signed transaction with raw transaction hex
|
|
210
|
+
*/
|
|
211
|
+
async sign(wallet) {
|
|
212
|
+
const hash = this.signature_hash();
|
|
213
|
+
const sig = await wallet.sign_hash(hash);
|
|
214
|
+
|
|
215
|
+
// For 0x7a, we use y_parity (0 or 1) directly in the signature
|
|
216
|
+
const y_parity = sig.v;
|
|
217
|
+
|
|
218
|
+
const tx = this.build();
|
|
219
|
+
|
|
220
|
+
// Build signed transaction list (for EIP-2718 encoding)
|
|
221
|
+
const signed_fields = [
|
|
222
|
+
num_to_hex(tx.chain_id, 32),
|
|
223
|
+
num_to_hex(tx.nonce, 32),
|
|
224
|
+
num_to_hex(tx.max_priority_fee_per_gas, 32),
|
|
225
|
+
num_to_hex(tx.max_fee_per_gas, 32),
|
|
226
|
+
num_to_hex(tx.gas_limit, 32),
|
|
227
|
+
tx.to,
|
|
228
|
+
num_to_hex(tx.value, 32),
|
|
229
|
+
tx.data,
|
|
230
|
+
'0xc0', // empty access list
|
|
231
|
+
tx.fee_token,
|
|
232
|
+
tx.fee_payer,
|
|
233
|
+
num_to_hex(tx.max_fee_per_gas_usd_attodollars, 32),
|
|
234
|
+
tx.fee_payer_signature,
|
|
235
|
+
// Signature: y_parity (1 byte), r (32 bytes), s (32 bytes)
|
|
236
|
+
num_to_hex(y_parity, 1),
|
|
237
|
+
sig.r,
|
|
238
|
+
sig.s,
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
// EIP-2718: first byte is type (0x7a), then RLP encoded fields
|
|
242
|
+
const rlp_fields = rlp_encode(signed_fields);
|
|
243
|
+
const type_byte = '0x7a';
|
|
244
|
+
const raw_transaction = type_byte + rlp_fields.slice(2); // Remove 0x prefix from RLP output
|
|
245
|
+
|
|
246
|
+
const transaction_hash = keccak256(raw_transaction);
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
...tx,
|
|
250
|
+
v: y_parity,
|
|
251
|
+
r: sig.r,
|
|
252
|
+
s: sig.s,
|
|
253
|
+
raw_transaction,
|
|
254
|
+
transaction_hash,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Create a new transaction builder
|
|
261
|
+
* @returns {TxBuilder}
|
|
262
|
+
*/
|
|
263
|
+
export function create_transaction() {
|
|
264
|
+
return new TxBuilder();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Calculate the hash that fee_payer should sign.
|
|
269
|
+
*
|
|
270
|
+
* This binds the sponsorship to:
|
|
271
|
+
* - Specific chain (chain_id) - prevents replay across chains
|
|
272
|
+
* - Specific sender - only this sender can use the sponsorship
|
|
273
|
+
* - Specific fee token - prevents cross-token attacks
|
|
274
|
+
* - Specific fee parameters - bounds the maximum fee
|
|
275
|
+
*
|
|
276
|
+
* @param {object} tx - Transaction object with chain_id, nonce, gas_limit, fee_token, fee_payer, max_fee_per_gas_usd_attodollars
|
|
277
|
+
* @param {string} sender - Sender address as 0x-prefixed hex
|
|
278
|
+
* @returns {string} The 32-byte hash that the fee_payer should sign
|
|
279
|
+
*/
|
|
280
|
+
export function fee_payer_signature_hash(tx, sender) {
|
|
281
|
+
// RLP encode the fields as a list
|
|
282
|
+
// [chain_id, nonce, gas_limit, fee_token, fee_payer, max_fee_per_gas_usd_attodollars, sender]
|
|
283
|
+
const list = [
|
|
284
|
+
num_to_hex(tx.chain_id, 32),
|
|
285
|
+
num_to_hex(tx.nonce, 32),
|
|
286
|
+
num_to_hex(tx.gas_limit, 32),
|
|
287
|
+
tx.fee_token,
|
|
288
|
+
tx.fee_payer,
|
|
289
|
+
num_to_hex(tx.max_fee_per_gas_usd_attodollars, 32),
|
|
290
|
+
sender,
|
|
291
|
+
];
|
|
292
|
+
|
|
293
|
+
const encoded = rlp_encode(list);
|
|
294
|
+
|
|
295
|
+
// Prepend magic byte 0x7B
|
|
296
|
+
const with_magic = FEE_PAYER_SIGNATURE_MAGIC_BYTE + encoded.slice(2);
|
|
297
|
+
|
|
298
|
+
return keccak256(with_magic);
|
|
299
|
+
}
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for the SDK
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import sha3 from 'js-sha3';
|
|
6
|
+
import RLP from 'rlp';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Compute keccak256 hash
|
|
10
|
+
* @param {string|Buffer} data - Input data
|
|
11
|
+
* @returns {string} Hash as 0x-prefixed hex string
|
|
12
|
+
*/
|
|
13
|
+
export function keccak256(data) {
|
|
14
|
+
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data.slice(2), 'hex');
|
|
15
|
+
const hash = sha3.keccak_256(buf);
|
|
16
|
+
return '0x' + hash;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Pad a hex string to specified bytes
|
|
21
|
+
* @param {string} hex - Hex string (with or without 0x prefix)
|
|
22
|
+
* @param {number} bytes - Target byte length
|
|
23
|
+
* @returns {string} Padded hex string
|
|
24
|
+
*/
|
|
25
|
+
export function pad_hex(hex, bytes) {
|
|
26
|
+
const clean = hex.startsWith('0x') ? hex.slice(2) : hex;
|
|
27
|
+
return '0x' + clean.padStart(bytes * 2, '0');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Convert number to padded hex
|
|
32
|
+
* @param {number|BigInt} num - Number to convert
|
|
33
|
+
* @param {number} bytes - Target byte length
|
|
34
|
+
* @returns {string} Padded hex string
|
|
35
|
+
*/
|
|
36
|
+
export function num_to_hex(num, bytes = 32) {
|
|
37
|
+
const hex = num.toString(16);
|
|
38
|
+
return pad_hex('0x' + hex, bytes);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Simple RLP encoding (for 0x7a transactions)
|
|
43
|
+
* @param {Array} items - Array of items to encode
|
|
44
|
+
* @returns {string} RLP encoded data as 0x-prefixed hex
|
|
45
|
+
*/
|
|
46
|
+
export function rlp_encode(items) {
|
|
47
|
+
const encoded = RLP.encode(items);
|
|
48
|
+
return '0x' + Buffer.from(encoded).toString('hex');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Compute transaction hash from signed transaction
|
|
53
|
+
* @param {object} signed_tx - Signed transaction
|
|
54
|
+
* @returns {string} Transaction hash
|
|
55
|
+
*/
|
|
56
|
+
export function transaction_hash(signed_tx) {
|
|
57
|
+
return keccak256(signed_tx);
|
|
58
|
+
}
|
package/src/wallet.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wallet - Local private key management and transaction signing
|
|
3
|
+
*
|
|
4
|
+
* IMPORTANT: Private keys never leave this class. All signing happens locally.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as secp256k1 from 'noble-secp256k1';
|
|
8
|
+
import { keccak256 } from './utils.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Generate a random private key
|
|
12
|
+
* @returns {string} Private key as 0x-prefixed hex string
|
|
13
|
+
*/
|
|
14
|
+
export function generate_private_key() {
|
|
15
|
+
const privateKey = secp256k1.utils.randomPrivateKey();
|
|
16
|
+
return '0x' + Buffer.from(privateKey).toString('hex');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Derive address from private key
|
|
21
|
+
* @param {string} private_key - Private key as 0x-prefixed hex string
|
|
22
|
+
* @returns {string} Address as 0x-prefixed hex string
|
|
23
|
+
*/
|
|
24
|
+
export function private_key_to_address(private_key) {
|
|
25
|
+
const pk = private_key.slice(2);
|
|
26
|
+
const publicKey = secp256k1.getPublicKey(pk, true);
|
|
27
|
+
// Remove first byte (prefix) and take last 64 bytes (x, y)
|
|
28
|
+
const hash = keccak256(publicKey.slice(1));
|
|
29
|
+
// Take last 20 bytes
|
|
30
|
+
return '0x' + hash.slice(-40);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parse DER signature to r, s components
|
|
35
|
+
* @param {string} der - DER encoded signature (hex string)
|
|
36
|
+
* @returns {{r: string, s: string}}
|
|
37
|
+
*/
|
|
38
|
+
function parse_der(der) {
|
|
39
|
+
// DER format: 30 [totalLen] 02 [rLen] [r] 02 [sLen] [s]
|
|
40
|
+
// der is a hex string (without 0x prefix)
|
|
41
|
+
let offset = 0;
|
|
42
|
+
|
|
43
|
+
// Skip 0x30
|
|
44
|
+
if (der.slice(offset, offset + 2) !== '30') throw new Error('Invalid DER: ' + der.slice(0, 10));
|
|
45
|
+
offset += 2;
|
|
46
|
+
|
|
47
|
+
// Skip total length
|
|
48
|
+
offset += 2;
|
|
49
|
+
|
|
50
|
+
// Check 0x02 (r)
|
|
51
|
+
if (der.slice(offset, offset + 2) !== '02') throw new Error('Invalid DER: expected 02');
|
|
52
|
+
offset += 2;
|
|
53
|
+
|
|
54
|
+
// Get r length
|
|
55
|
+
const rLen = parseInt(der.slice(offset, offset + 2), 16);
|
|
56
|
+
offset += 2;
|
|
57
|
+
|
|
58
|
+
// Get r (may need padding for leading zeros)
|
|
59
|
+
const rHex = der.slice(offset, offset + rLen * 2);
|
|
60
|
+
const r = '0x' + rHex.padStart(64, '0');
|
|
61
|
+
offset += rLen * 2;
|
|
62
|
+
|
|
63
|
+
// Check 0x02 (s)
|
|
64
|
+
if (der.slice(offset, offset + 2) !== '02') throw new Error('Invalid DER: expected 02');
|
|
65
|
+
offset += 2;
|
|
66
|
+
|
|
67
|
+
// Get s length
|
|
68
|
+
const sLen = parseInt(der.slice(offset, offset + 2), 16);
|
|
69
|
+
offset += 2;
|
|
70
|
+
|
|
71
|
+
// Get s
|
|
72
|
+
const sHex = der.slice(offset, offset + sLen * 2);
|
|
73
|
+
const s = '0x' + sHex.padStart(64, '0');
|
|
74
|
+
|
|
75
|
+
return { r, s };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Sign a transaction hash with private key
|
|
80
|
+
* @param {string} hash - Transaction hash as 0x-prefixed hex
|
|
81
|
+
* @param {string} private_key - Private key as 0x-prefixed hex
|
|
82
|
+
* @returns {{r: string, s: string, v: number}} Signature components
|
|
83
|
+
*/
|
|
84
|
+
export async function sign_hash(hash, private_key) {
|
|
85
|
+
const pk = private_key.slice(2);
|
|
86
|
+
const [derSig, recovery] = await secp256k1.sign(hash.slice(2), pk, { canonical: true, recovered: true });
|
|
87
|
+
const { r, s } = parse_der(derSig);
|
|
88
|
+
return { r, s, v: recovery };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Wallet class for local transaction signing
|
|
93
|
+
*/
|
|
94
|
+
export class Wallet {
|
|
95
|
+
/**
|
|
96
|
+
* Create a wallet from an existing private key
|
|
97
|
+
* @param {string} private_key - Private key as 0x-prefixed hex string
|
|
98
|
+
*/
|
|
99
|
+
constructor(private_key) {
|
|
100
|
+
if (!private_key.startsWith('0x') || private_key.length !== 66) {
|
|
101
|
+
throw new Error('Invalid private key format');
|
|
102
|
+
}
|
|
103
|
+
this.private_key = private_key;
|
|
104
|
+
this.address = private_key_to_address(private_key);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Generate a new random wallet
|
|
109
|
+
* @returns {Wallet}
|
|
110
|
+
*/
|
|
111
|
+
static generate() {
|
|
112
|
+
return new Wallet(generate_private_key());
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Sign a transaction hash
|
|
117
|
+
* @param {string} hash - Transaction hash as 0x-prefixed hex
|
|
118
|
+
* @returns {{r: string, s: string, v: number}} Signature
|
|
119
|
+
*/
|
|
120
|
+
async sign_hash(hash) {
|
|
121
|
+
return sign_hash(hash, this.private_key);
|
|
122
|
+
}
|
|
123
|
+
}
|