@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 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
+ }
@@ -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
+ }