nara-sdk 1.0.74 → 1.0.75
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 +146 -13
- package/index.ts +49 -0
- package/package.json +1 -1
- package/src/bridge.ts +780 -0
- package/src/constants.ts +20 -0
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
-
TypeScript/JavaScript SDK for interacting with the Nara blockchain. Build agents, submit transactions, query accounts, and integrate with on-chain programs.
|
|
14
|
+
TypeScript/JavaScript SDK for interacting with the Nara blockchain. Build agents, submit transactions, query accounts, cross-chain bridge, and integrate with on-chain programs.
|
|
15
15
|
|
|
16
16
|
## Install
|
|
17
17
|
|
|
@@ -19,29 +19,162 @@ TypeScript/JavaScript SDK for interacting with the Nara blockchain. Build agents
|
|
|
19
19
|
npm install nara-sdk
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
+
## Features
|
|
23
|
+
|
|
24
|
+
- **Agent Registry** — Register agents, bind Twitter, submit tweets, verify, referral system
|
|
25
|
+
- **Quest (PoMI)** — Proof of Machine Intelligence ZK quest system, stake, answer-to-earn
|
|
26
|
+
- **Skills Hub** — On-chain skill registry for AI agents, upload/query skill content
|
|
27
|
+
- **ZK ID** — Zero-knowledge anonymous identity, deposit, withdraw, ownership proofs
|
|
28
|
+
- **Cross-chain Bridge** — Nara ↔ Solana bridge via Hyperlane warp routes (USDC, SOL), with in-tx fee extraction and validator signature tracking
|
|
29
|
+
|
|
22
30
|
## Quick Start
|
|
23
31
|
|
|
24
|
-
```
|
|
25
|
-
import { Connection, Keypair
|
|
32
|
+
```ts
|
|
33
|
+
import { Connection, Keypair } from '@solana/web3.js';
|
|
34
|
+
import { getQuestInfo, submitAnswer, generateProof } from 'nara-sdk';
|
|
26
35
|
|
|
27
36
|
const connection = new Connection('https://mainnet-api.nara.build');
|
|
28
|
-
const balance = await connection.getBalance(publicKey);
|
|
29
37
|
```
|
|
30
38
|
|
|
31
|
-
##
|
|
39
|
+
## Cross-chain Bridge
|
|
40
|
+
|
|
41
|
+
Bridge tokens between Solana and Nara with built-in 0.5% fee extraction.
|
|
42
|
+
|
|
43
|
+
### One-step bridge
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { Connection, Keypair } from '@solana/web3.js';
|
|
47
|
+
import { bridgeTransfer, setAltAddress } from 'nara-sdk';
|
|
48
|
+
|
|
49
|
+
const solanaConn = new Connection('https://api.mainnet-beta.solana.com');
|
|
50
|
+
|
|
51
|
+
// Disable Nara ALT when sending from Solana
|
|
52
|
+
setAltAddress(null);
|
|
53
|
+
|
|
54
|
+
const result = await bridgeTransfer(solanaConn, wallet, {
|
|
55
|
+
token: 'USDC', // 'USDC' | 'SOL'
|
|
56
|
+
fromChain: 'solana', // 'solana' | 'nara'
|
|
57
|
+
recipient: targetPubkey, // destination chain address
|
|
58
|
+
amount: 1_000_000n, // raw units (1 USDC = 1_000_000)
|
|
59
|
+
});
|
|
32
60
|
|
|
61
|
+
console.log(result.signature); // source chain tx
|
|
62
|
+
console.log(result.messageId); // cross-chain message ID (0x...)
|
|
63
|
+
console.log(result.feeAmount); // fee deducted
|
|
64
|
+
console.log(result.bridgeAmount); // net amount bridged
|
|
33
65
|
```
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
66
|
+
|
|
67
|
+
### Build instructions for relay
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { makeBridgeIxs } from 'nara-sdk';
|
|
71
|
+
|
|
72
|
+
const { instructions, uniqueMessageKeypair, feeAmount, bridgeAmount } =
|
|
73
|
+
makeBridgeIxs({
|
|
74
|
+
token: 'USDC',
|
|
75
|
+
fromChain: 'solana',
|
|
76
|
+
sender: userPubkey,
|
|
77
|
+
recipient: targetPubkey,
|
|
78
|
+
amount: 1_000_000n,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// uniqueMessageKeypair must sign the tx
|
|
39
82
|
```
|
|
40
83
|
|
|
41
|
-
|
|
84
|
+
### Track cross-chain message
|
|
42
85
|
|
|
43
|
-
```
|
|
44
|
-
|
|
86
|
+
```ts
|
|
87
|
+
import {
|
|
88
|
+
extractMessageId,
|
|
89
|
+
queryMessageSignatures,
|
|
90
|
+
queryMessageStatus,
|
|
91
|
+
} from 'nara-sdk';
|
|
92
|
+
|
|
93
|
+
// 1. Extract message ID from source tx
|
|
94
|
+
const messageId = await extractMessageId(connection, signature);
|
|
95
|
+
|
|
96
|
+
// 2. Query validator signatures (3-way parallel scan on S3)
|
|
97
|
+
const sigs = await queryMessageSignatures(messageId, 'solana');
|
|
98
|
+
console.log(sigs.signedCount, '/', sigs.totalValidators); // e.g. 3/3
|
|
99
|
+
console.log(sigs.fullySigned); // true
|
|
100
|
+
|
|
101
|
+
// 3. Check delivery on destination chain
|
|
102
|
+
const status = await queryMessageStatus(naraConn, messageId, 'nara');
|
|
103
|
+
console.log(status.delivered); // true
|
|
104
|
+
console.log(status.deliverySignature); // destination tx
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Supported tokens
|
|
108
|
+
|
|
109
|
+
| Token | Solana side | Nara side | Decimals |
|
|
110
|
+
|---|---|---|---|
|
|
111
|
+
| USDC | collateral (lock) | synthetic (mint, Token-2022) | 6 |
|
|
112
|
+
| SOL | native (lamports) | synthetic (mint, Token-2022) | 9 |
|
|
113
|
+
|
|
114
|
+
Add new tokens at runtime:
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import { registerBridgeToken } from 'nara-sdk';
|
|
118
|
+
|
|
119
|
+
registerBridgeToken('USDT', {
|
|
120
|
+
symbol: 'USDT',
|
|
121
|
+
decimals: 6,
|
|
122
|
+
solana: { warpProgram, mode: 'collateral', mint, tokenProgram },
|
|
123
|
+
nara: { warpProgram, mode: 'synthetic', mint, tokenProgram },
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Fee configuration
|
|
128
|
+
|
|
129
|
+
Default fee: **0.5%** (50 bps), deducted from the bridged amount on the source chain.
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { setBridgeFeeRecipient } from 'nara-sdk';
|
|
133
|
+
|
|
134
|
+
// Override fee recipient at runtime
|
|
135
|
+
setBridgeFeeRecipient('YourFeeRecipientPubkey...');
|
|
136
|
+
|
|
137
|
+
// Or per-call
|
|
138
|
+
await bridgeTransfer(conn, wallet, {
|
|
139
|
+
...params,
|
|
140
|
+
feeBps: 100, // 1%
|
|
141
|
+
feeRecipient: customPubkey, // override
|
|
142
|
+
skipFee: true, // or skip entirely
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Agent Registry
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
import {
|
|
150
|
+
registerAgent,
|
|
151
|
+
getAgentRecord,
|
|
152
|
+
setTwitter,
|
|
153
|
+
verifyTwitter,
|
|
154
|
+
submitTweet,
|
|
155
|
+
approveTweet,
|
|
156
|
+
} from 'nara-sdk';
|
|
157
|
+
|
|
158
|
+
// Register an agent
|
|
159
|
+
await registerAgent(connection, wallet, agentId, name, metadataUri);
|
|
160
|
+
|
|
161
|
+
// Twitter verification flow
|
|
162
|
+
await setTwitter(connection, wallet, agentId, handle);
|
|
163
|
+
await verifyTwitter(connection, verifierWallet, agentId);
|
|
164
|
+
|
|
165
|
+
// Tweet submission & approval
|
|
166
|
+
await submitTweet(connection, wallet, agentId, tweetId, tweetUrl);
|
|
167
|
+
await approveTweet(connection, verifierWallet, agentId, tweetId, freeCredits);
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Quest (PoMI)
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
import { getQuestInfo, generateProof, submitAnswer } from 'nara-sdk';
|
|
174
|
+
|
|
175
|
+
const quest = await getQuestInfo(connection);
|
|
176
|
+
const proof = await generateProof(quest.question, answer);
|
|
177
|
+
const sig = await submitAnswer(connection, wallet, proof);
|
|
45
178
|
```
|
|
46
179
|
|
|
47
180
|
## Documentation
|
package/index.ts
CHANGED
|
@@ -15,6 +15,9 @@ export {
|
|
|
15
15
|
DEFAULT_ZKID_PROGRAM_ID,
|
|
16
16
|
DEFAULT_AGENT_REGISTRY_PROGRAM_ID,
|
|
17
17
|
DEFAULT_ALT_ADDRESS,
|
|
18
|
+
DEFAULT_BRIDGE_FEE_BPS,
|
|
19
|
+
DEFAULT_BRIDGE_FEE_RECIPIENT,
|
|
20
|
+
BRIDGE_FEE_BPS_DENOMINATOR,
|
|
18
21
|
} from "./src/constants";
|
|
19
22
|
|
|
20
23
|
// Export signing utilities
|
|
@@ -165,6 +168,52 @@ export {
|
|
|
165
168
|
type AgentRegistryOptions,
|
|
166
169
|
} from "./src/agent_registry";
|
|
167
170
|
|
|
171
|
+
// Export bridge functions and types
|
|
172
|
+
export {
|
|
173
|
+
// Constants
|
|
174
|
+
SOLANA_DOMAIN,
|
|
175
|
+
NARA_DOMAIN,
|
|
176
|
+
SOLANA_MAILBOX,
|
|
177
|
+
NARA_MAILBOX,
|
|
178
|
+
SPL_NOOP,
|
|
179
|
+
BRIDGE_TOKENS,
|
|
180
|
+
// Token registry
|
|
181
|
+
registerBridgeToken,
|
|
182
|
+
// Fee recipient runtime override
|
|
183
|
+
setBridgeFeeRecipient,
|
|
184
|
+
getBridgeFeeRecipient,
|
|
185
|
+
// PDA helpers
|
|
186
|
+
deriveOutboxPda,
|
|
187
|
+
deriveDispatchAuthorityPda,
|
|
188
|
+
deriveDispatchedMessagePda,
|
|
189
|
+
deriveTokenPda,
|
|
190
|
+
deriveEscrowPda,
|
|
191
|
+
deriveNativeCollateralPda,
|
|
192
|
+
// Encoders
|
|
193
|
+
encodeTransferRemote,
|
|
194
|
+
// Fee + ix builders
|
|
195
|
+
calculateBridgeFee,
|
|
196
|
+
makeBridgeFeeIxs,
|
|
197
|
+
makeTransferRemoteIx,
|
|
198
|
+
makeBridgeIxs,
|
|
199
|
+
// High level
|
|
200
|
+
bridgeTransfer,
|
|
201
|
+
extractMessageId,
|
|
202
|
+
queryMessageStatus,
|
|
203
|
+
queryMessageSignatures,
|
|
204
|
+
type BridgeChain,
|
|
205
|
+
type BridgeMode,
|
|
206
|
+
type BridgeTokenSide,
|
|
207
|
+
type BridgeTokenConfig,
|
|
208
|
+
type BridgeTransferParams,
|
|
209
|
+
type BridgeIxsResult,
|
|
210
|
+
type BridgeTransferResult,
|
|
211
|
+
type FeeSplit,
|
|
212
|
+
type MessageStatus,
|
|
213
|
+
type ValidatorSignature,
|
|
214
|
+
type MessageSignatureStatus,
|
|
215
|
+
} from "./src/bridge";
|
|
216
|
+
|
|
168
217
|
// Export IDLs and types
|
|
169
218
|
export { default as NaraQuestIDL } from "./src/idls/nara_quest.json";
|
|
170
219
|
export { default as NaraSkillsHubIDL } from "./src/idls/nara_skills_hub.json";
|
package/package.json
CHANGED
package/src/bridge.ts
ADDED
|
@@ -0,0 +1,780 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-chain bridge SDK between Nara and Solana via Hyperlane warp routes.
|
|
3
|
+
*
|
|
4
|
+
* Supports two assets out of the box (USDC, SOL) in both directions, with
|
|
5
|
+
* an in-tx fee deduction (default 0.5%) sent to BRIDGE_FEE_RECIPIENT.
|
|
6
|
+
*
|
|
7
|
+
* Adding a new asset:
|
|
8
|
+
* 1. Append a new entry to BRIDGE_TOKENS with {symbol, decimals, solana, nara}.
|
|
9
|
+
* 2. The PDA derivation, account-list construction, and instruction encoding
|
|
10
|
+
* are mode-driven (collateral / synthetic / native), no per-token code paths.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
Connection,
|
|
15
|
+
Keypair,
|
|
16
|
+
PublicKey,
|
|
17
|
+
SystemProgram,
|
|
18
|
+
TransactionInstruction,
|
|
19
|
+
} from "@solana/web3.js";
|
|
20
|
+
import {
|
|
21
|
+
createAssociatedTokenAccountIdempotentInstruction,
|
|
22
|
+
createTransferCheckedInstruction,
|
|
23
|
+
getAssociatedTokenAddressSync,
|
|
24
|
+
TOKEN_2022_PROGRAM_ID,
|
|
25
|
+
TOKEN_PROGRAM_ID,
|
|
26
|
+
} from "@solana/spl-token";
|
|
27
|
+
import {
|
|
28
|
+
BRIDGE_FEE_BPS_DENOMINATOR,
|
|
29
|
+
DEFAULT_BRIDGE_FEE_BPS,
|
|
30
|
+
DEFAULT_BRIDGE_FEE_RECIPIENT,
|
|
31
|
+
} from "./constants";
|
|
32
|
+
import { sendTx } from "./tx";
|
|
33
|
+
|
|
34
|
+
// ─── Types ────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
export type BridgeChain = "solana" | "nara";
|
|
37
|
+
export type BridgeMode = "collateral" | "synthetic" | "native";
|
|
38
|
+
|
|
39
|
+
export interface BridgeTokenSide {
|
|
40
|
+
warpProgram: PublicKey;
|
|
41
|
+
mode: BridgeMode;
|
|
42
|
+
/** SPL mint pubkey. null for Solana native SOL. */
|
|
43
|
+
mint: PublicKey | null;
|
|
44
|
+
/** SPL token program. null for Solana native SOL. */
|
|
45
|
+
tokenProgram: PublicKey | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface BridgeTokenConfig {
|
|
49
|
+
symbol: string;
|
|
50
|
+
decimals: number;
|
|
51
|
+
solana: BridgeTokenSide;
|
|
52
|
+
nara: BridgeTokenSide;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface BridgeTransferParams {
|
|
56
|
+
/** Token symbol from BRIDGE_TOKENS (e.g. "USDC", "SOL") */
|
|
57
|
+
token: string;
|
|
58
|
+
/** Source chain — fee is deducted on this chain in the source token */
|
|
59
|
+
fromChain: BridgeChain;
|
|
60
|
+
/** Sender pubkey on the source chain */
|
|
61
|
+
sender: PublicKey;
|
|
62
|
+
/** Recipient pubkey on the destination chain */
|
|
63
|
+
recipient: PublicKey;
|
|
64
|
+
/** Gross amount (raw units, source-chain decimals). Fee is deducted from this. */
|
|
65
|
+
amount: bigint;
|
|
66
|
+
/** Optional fee bps override (defaults to BRIDGE_FEE_BPS) */
|
|
67
|
+
feeBps?: number;
|
|
68
|
+
/** Optional fee recipient override (defaults to BRIDGE_FEE_RECIPIENT) */
|
|
69
|
+
feeRecipient?: PublicKey;
|
|
70
|
+
/** Skip fee deduction entirely */
|
|
71
|
+
skipFee?: boolean;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface BridgeIxsResult {
|
|
75
|
+
/** All instructions in order: [feeIx?, atapayerIx?, transferRemoteIx] */
|
|
76
|
+
instructions: TransactionInstruction[];
|
|
77
|
+
/** Extra signer required by the transferRemote ix (the unique message keypair) */
|
|
78
|
+
uniqueMessageKeypair: Keypair;
|
|
79
|
+
/** Fee deducted in source token raw units */
|
|
80
|
+
feeAmount: bigint;
|
|
81
|
+
/** Net amount actually bridged */
|
|
82
|
+
bridgeAmount: bigint;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface BridgeTransferResult {
|
|
86
|
+
signature: string;
|
|
87
|
+
messageId: string | null;
|
|
88
|
+
feeAmount: bigint;
|
|
89
|
+
bridgeAmount: bigint;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ─── Chain constants ───────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
export const SOLANA_DOMAIN = 1399811149;
|
|
95
|
+
export const NARA_DOMAIN = 40778959;
|
|
96
|
+
|
|
97
|
+
export const SOLANA_MAILBOX = new PublicKey(
|
|
98
|
+
"E588QtVUvresuXq2KoNEwAmoifCzYGpRBdHByN9KQMbi"
|
|
99
|
+
);
|
|
100
|
+
export const NARA_MAILBOX = new PublicKey(
|
|
101
|
+
"EjtLD3MCBJregFKAce2pQqPtSnnmBWK5oAZ3wBifHnaH"
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
export const SPL_NOOP = new PublicKey(
|
|
105
|
+
"noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV"
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
function mailboxFor(chain: BridgeChain): PublicKey {
|
|
109
|
+
return chain === "solana" ? SOLANA_MAILBOX : NARA_MAILBOX;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function destinationDomainFor(toChain: BridgeChain): number {
|
|
113
|
+
return toChain === "solana" ? SOLANA_DOMAIN : NARA_DOMAIN;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ─── Token registry ───────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
export const BRIDGE_TOKENS: Record<string, BridgeTokenConfig> = {
|
|
119
|
+
USDC: {
|
|
120
|
+
symbol: "USDC",
|
|
121
|
+
decimals: 6,
|
|
122
|
+
solana: {
|
|
123
|
+
warpProgram: new PublicKey("4GcZJTa8s9vxtTz97Vj1RrwKMqPkT3DiiJkvUQDwsuZP"),
|
|
124
|
+
mode: "collateral",
|
|
125
|
+
mint: new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"),
|
|
126
|
+
tokenProgram: TOKEN_PROGRAM_ID,
|
|
127
|
+
},
|
|
128
|
+
nara: {
|
|
129
|
+
warpProgram: new PublicKey("BC2j6WrdPs9xhU9CfBwJsYSnJrGq5Tcm4SEen9ENv7go"),
|
|
130
|
+
mode: "synthetic",
|
|
131
|
+
mint: new PublicKey("8P7UGWjq86N3WUmwEgKeGHJZLcoMJqr5jnRUmeBN7YwR"),
|
|
132
|
+
tokenProgram: TOKEN_2022_PROGRAM_ID,
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
SOL: {
|
|
136
|
+
symbol: "SOL",
|
|
137
|
+
decimals: 9,
|
|
138
|
+
solana: {
|
|
139
|
+
warpProgram: new PublicKey("46MmAWwKRAt9uvn7m44NXbVq2DCWBQE2r1TDw25nyXrt"),
|
|
140
|
+
mode: "native",
|
|
141
|
+
mint: null,
|
|
142
|
+
tokenProgram: null,
|
|
143
|
+
},
|
|
144
|
+
nara: {
|
|
145
|
+
warpProgram: new PublicKey("6bKmjEMbjcJUnqAiNw7AXuMvUALzw5XRKiV9dBsterxg"),
|
|
146
|
+
mode: "synthetic",
|
|
147
|
+
mint: new PublicKey("7fKh7DqPZmsYPHdGvt9Qw2rZkSEGp9F5dBa3XuuuhavU"),
|
|
148
|
+
tokenProgram: TOKEN_2022_PROGRAM_ID,
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/** Register a new bridge token at runtime */
|
|
154
|
+
export function registerBridgeToken(symbol: string, config: BridgeTokenConfig): void {
|
|
155
|
+
BRIDGE_TOKENS[symbol] = config;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function getToken(symbol: string): BridgeTokenConfig {
|
|
159
|
+
const t = BRIDGE_TOKENS[symbol];
|
|
160
|
+
if (!t) throw new Error(`Unknown bridge token: ${symbol}`);
|
|
161
|
+
return t;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ─── Fee recipient (runtime override) ─────────────────────────────
|
|
165
|
+
|
|
166
|
+
let _feeRecipientOverride: PublicKey | null = null;
|
|
167
|
+
|
|
168
|
+
/** Override the bridge fee recipient at runtime */
|
|
169
|
+
export function setBridgeFeeRecipient(recipient: PublicKey | string | null): void {
|
|
170
|
+
if (recipient === null) {
|
|
171
|
+
_feeRecipientOverride = null;
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
_feeRecipientOverride =
|
|
175
|
+
typeof recipient === "string" ? new PublicKey(recipient) : recipient;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function getBridgeFeeRecipient(): PublicKey {
|
|
179
|
+
if (_feeRecipientOverride) return _feeRecipientOverride;
|
|
180
|
+
return new PublicKey(DEFAULT_BRIDGE_FEE_RECIPIENT);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ─── PDA derivation ───────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
export function deriveOutboxPda(mailbox: PublicKey): PublicKey {
|
|
186
|
+
const [pda] = PublicKey.findProgramAddressSync(
|
|
187
|
+
[Buffer.from("hyperlane"), Buffer.from("-"), Buffer.from("outbox")],
|
|
188
|
+
mailbox
|
|
189
|
+
);
|
|
190
|
+
return pda;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function deriveDispatchAuthorityPda(warpProgram: PublicKey): PublicKey {
|
|
194
|
+
const [pda] = PublicKey.findProgramAddressSync(
|
|
195
|
+
[
|
|
196
|
+
Buffer.from("hyperlane_dispatcher"),
|
|
197
|
+
Buffer.from("-"),
|
|
198
|
+
Buffer.from("dispatch_authority"),
|
|
199
|
+
],
|
|
200
|
+
warpProgram
|
|
201
|
+
);
|
|
202
|
+
return pda;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function deriveDispatchedMessagePda(
|
|
206
|
+
mailbox: PublicKey,
|
|
207
|
+
uniqueMessagePubkey: PublicKey
|
|
208
|
+
): PublicKey {
|
|
209
|
+
const [pda] = PublicKey.findProgramAddressSync(
|
|
210
|
+
[
|
|
211
|
+
Buffer.from("hyperlane"),
|
|
212
|
+
Buffer.from("-"),
|
|
213
|
+
Buffer.from("dispatched_message"),
|
|
214
|
+
Buffer.from("-"),
|
|
215
|
+
uniqueMessagePubkey.toBuffer(),
|
|
216
|
+
],
|
|
217
|
+
mailbox
|
|
218
|
+
);
|
|
219
|
+
return pda;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function deriveTokenPda(warpProgram: PublicKey): PublicKey {
|
|
223
|
+
const [pda] = PublicKey.findProgramAddressSync(
|
|
224
|
+
[
|
|
225
|
+
Buffer.from("hyperlane_message_recipient"),
|
|
226
|
+
Buffer.from("-"),
|
|
227
|
+
Buffer.from("handle"),
|
|
228
|
+
Buffer.from("-"),
|
|
229
|
+
Buffer.from("account_metas"),
|
|
230
|
+
],
|
|
231
|
+
warpProgram
|
|
232
|
+
);
|
|
233
|
+
return pda;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function deriveEscrowPda(warpProgram: PublicKey): PublicKey {
|
|
237
|
+
const [pda] = PublicKey.findProgramAddressSync(
|
|
238
|
+
[
|
|
239
|
+
Buffer.from("hyperlane_token"),
|
|
240
|
+
Buffer.from("-"),
|
|
241
|
+
Buffer.from("escrow"),
|
|
242
|
+
],
|
|
243
|
+
warpProgram
|
|
244
|
+
);
|
|
245
|
+
return pda;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function deriveNativeCollateralPda(warpProgram: PublicKey): PublicKey {
|
|
249
|
+
const [pda] = PublicKey.findProgramAddressSync(
|
|
250
|
+
[
|
|
251
|
+
Buffer.from("hyperlane_token"),
|
|
252
|
+
Buffer.from("-"),
|
|
253
|
+
Buffer.from("native_collateral"),
|
|
254
|
+
],
|
|
255
|
+
warpProgram
|
|
256
|
+
);
|
|
257
|
+
return pda;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ─── Instruction data encoder ─────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Encodes a TransferRemote instruction body.
|
|
264
|
+
* Layout:
|
|
265
|
+
* [9 bytes prefix 0x01*9] [4 bytes destinationDomain u32 LE]
|
|
266
|
+
* [32 bytes recipient] [32 bytes amount as U256 LE]
|
|
267
|
+
*/
|
|
268
|
+
export function encodeTransferRemote(
|
|
269
|
+
destinationDomain: number,
|
|
270
|
+
recipient: PublicKey,
|
|
271
|
+
amount: bigint
|
|
272
|
+
): Buffer {
|
|
273
|
+
const buf = Buffer.alloc(9 + 4 + 32 + 32);
|
|
274
|
+
for (let i = 0; i < 9; i++) buf[i] = 0x01;
|
|
275
|
+
buf.writeUInt32LE(destinationDomain, 9);
|
|
276
|
+
recipient.toBuffer().copy(buf, 13);
|
|
277
|
+
buf.writeBigUInt64LE(amount, 45);
|
|
278
|
+
return buf;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ─── Fee calculation ──────────────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
export interface FeeSplit {
|
|
284
|
+
feeAmount: bigint;
|
|
285
|
+
bridgeAmount: bigint;
|
|
286
|
+
feeBps: number;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function calculateBridgeFee(amount: bigint, feeBps?: number): FeeSplit {
|
|
290
|
+
const bps = feeBps ?? DEFAULT_BRIDGE_FEE_BPS;
|
|
291
|
+
if (bps < 0 || bps > BRIDGE_FEE_BPS_DENOMINATOR) {
|
|
292
|
+
throw new Error(`Invalid feeBps: ${bps}`);
|
|
293
|
+
}
|
|
294
|
+
const feeAmount = (amount * BigInt(bps)) / BigInt(BRIDGE_FEE_BPS_DENOMINATOR);
|
|
295
|
+
const bridgeAmount = amount - feeAmount;
|
|
296
|
+
return { feeAmount, bridgeAmount, feeBps: bps };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ─── Fee instruction builder ──────────────────────────────────────
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Build the fee-collection instructions on the source chain in the source token.
|
|
303
|
+
*
|
|
304
|
+
* For SPL tokens: returns [createIdempotentATA(feeRecipient), transferChecked].
|
|
305
|
+
* For native SOL on Solana: returns [SystemProgram.transfer].
|
|
306
|
+
*
|
|
307
|
+
* Returns empty array if feeAmount == 0.
|
|
308
|
+
*/
|
|
309
|
+
export function makeBridgeFeeIxs(params: {
|
|
310
|
+
token: string;
|
|
311
|
+
fromChain: BridgeChain;
|
|
312
|
+
sender: PublicKey;
|
|
313
|
+
feeRecipient: PublicKey;
|
|
314
|
+
feeAmount: bigint;
|
|
315
|
+
}): TransactionInstruction[] {
|
|
316
|
+
const { token, fromChain, sender, feeRecipient, feeAmount } = params;
|
|
317
|
+
if (feeAmount === 0n) return [];
|
|
318
|
+
|
|
319
|
+
const tokenCfg = getToken(token);
|
|
320
|
+
const side = tokenCfg[fromChain];
|
|
321
|
+
|
|
322
|
+
// Native SOL → SystemProgram.transfer
|
|
323
|
+
if (side.mode === "native") {
|
|
324
|
+
return [
|
|
325
|
+
SystemProgram.transfer({
|
|
326
|
+
fromPubkey: sender,
|
|
327
|
+
toPubkey: feeRecipient,
|
|
328
|
+
lamports: feeAmount,
|
|
329
|
+
}),
|
|
330
|
+
];
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// SPL token (collateral or synthetic) → ensure recipient ATA + transferChecked
|
|
334
|
+
if (!side.mint || !side.tokenProgram) {
|
|
335
|
+
throw new Error(`Token ${token} on ${fromChain} missing mint/tokenProgram`);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const senderAta = getAssociatedTokenAddressSync(
|
|
339
|
+
side.mint,
|
|
340
|
+
sender,
|
|
341
|
+
false,
|
|
342
|
+
side.tokenProgram
|
|
343
|
+
);
|
|
344
|
+
const recipientAta = getAssociatedTokenAddressSync(
|
|
345
|
+
side.mint,
|
|
346
|
+
feeRecipient,
|
|
347
|
+
true, // allowOwnerOffCurve — fee recipient may be a PDA
|
|
348
|
+
side.tokenProgram
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
return [
|
|
352
|
+
createAssociatedTokenAccountIdempotentInstruction(
|
|
353
|
+
sender,
|
|
354
|
+
recipientAta,
|
|
355
|
+
feeRecipient,
|
|
356
|
+
side.mint,
|
|
357
|
+
side.tokenProgram
|
|
358
|
+
),
|
|
359
|
+
createTransferCheckedInstruction(
|
|
360
|
+
senderAta,
|
|
361
|
+
side.mint,
|
|
362
|
+
recipientAta,
|
|
363
|
+
sender,
|
|
364
|
+
feeAmount,
|
|
365
|
+
tokenCfg.decimals,
|
|
366
|
+
[],
|
|
367
|
+
side.tokenProgram
|
|
368
|
+
),
|
|
369
|
+
];
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ─── TransferRemote instruction builder ───────────────────────────
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Build the warp route TransferRemote instruction for a given token+direction.
|
|
376
|
+
* The unique message keypair must be passed in (it's a required tx signer).
|
|
377
|
+
*/
|
|
378
|
+
export function makeTransferRemoteIx(params: {
|
|
379
|
+
token: string;
|
|
380
|
+
fromChain: BridgeChain;
|
|
381
|
+
sender: PublicKey;
|
|
382
|
+
recipient: PublicKey;
|
|
383
|
+
amount: bigint;
|
|
384
|
+
uniqueMessageKeypair: Keypair;
|
|
385
|
+
}): TransactionInstruction {
|
|
386
|
+
const { token, fromChain, sender, recipient, amount, uniqueMessageKeypair } =
|
|
387
|
+
params;
|
|
388
|
+
|
|
389
|
+
const tokenCfg = getToken(token);
|
|
390
|
+
const side = tokenCfg[fromChain];
|
|
391
|
+
const toChain: BridgeChain = fromChain === "solana" ? "nara" : "solana";
|
|
392
|
+
|
|
393
|
+
const mailbox = mailboxFor(fromChain);
|
|
394
|
+
const tokenPda = deriveTokenPda(side.warpProgram);
|
|
395
|
+
const dispatchAuthPda = deriveDispatchAuthorityPda(side.warpProgram);
|
|
396
|
+
const outboxPda = deriveOutboxPda(mailbox);
|
|
397
|
+
const dispatchedMsgPda = deriveDispatchedMessagePda(
|
|
398
|
+
mailbox,
|
|
399
|
+
uniqueMessageKeypair.publicKey
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
// First 9 accounts are common to all modes
|
|
403
|
+
const keys = [
|
|
404
|
+
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
|
|
405
|
+
{ pubkey: SPL_NOOP, isSigner: false, isWritable: false },
|
|
406
|
+
{ pubkey: tokenPda, isSigner: false, isWritable: false },
|
|
407
|
+
{ pubkey: mailbox, isSigner: false, isWritable: false },
|
|
408
|
+
{ pubkey: outboxPda, isSigner: false, isWritable: true },
|
|
409
|
+
{ pubkey: dispatchAuthPda, isSigner: false, isWritable: false },
|
|
410
|
+
{ pubkey: sender, isSigner: true, isWritable: true },
|
|
411
|
+
{ pubkey: uniqueMessageKeypair.publicKey, isSigner: true, isWritable: false },
|
|
412
|
+
{ pubkey: dispatchedMsgPda, isSigner: false, isWritable: true },
|
|
413
|
+
];
|
|
414
|
+
|
|
415
|
+
// Plugin-specific accounts
|
|
416
|
+
if (side.mode === "collateral") {
|
|
417
|
+
if (!side.mint || !side.tokenProgram) {
|
|
418
|
+
throw new Error(`Collateral mode requires mint+tokenProgram for ${token}`);
|
|
419
|
+
}
|
|
420
|
+
const senderAta = getAssociatedTokenAddressSync(
|
|
421
|
+
side.mint,
|
|
422
|
+
sender,
|
|
423
|
+
false,
|
|
424
|
+
side.tokenProgram
|
|
425
|
+
);
|
|
426
|
+
const escrowPda = deriveEscrowPda(side.warpProgram);
|
|
427
|
+
keys.push(
|
|
428
|
+
{ pubkey: side.tokenProgram, isSigner: false, isWritable: false },
|
|
429
|
+
{ pubkey: side.mint, isSigner: false, isWritable: true },
|
|
430
|
+
{ pubkey: senderAta, isSigner: false, isWritable: true },
|
|
431
|
+
{ pubkey: escrowPda, isSigner: false, isWritable: true }
|
|
432
|
+
);
|
|
433
|
+
} else if (side.mode === "synthetic") {
|
|
434
|
+
if (!side.mint || !side.tokenProgram) {
|
|
435
|
+
throw new Error(`Synthetic mode requires mint+tokenProgram for ${token}`);
|
|
436
|
+
}
|
|
437
|
+
const senderAta = getAssociatedTokenAddressSync(
|
|
438
|
+
side.mint,
|
|
439
|
+
sender,
|
|
440
|
+
false,
|
|
441
|
+
side.tokenProgram
|
|
442
|
+
);
|
|
443
|
+
keys.push(
|
|
444
|
+
{ pubkey: side.tokenProgram, isSigner: false, isWritable: false },
|
|
445
|
+
{ pubkey: side.mint, isSigner: false, isWritable: true },
|
|
446
|
+
{ pubkey: senderAta, isSigner: false, isWritable: true }
|
|
447
|
+
);
|
|
448
|
+
} else if (side.mode === "native") {
|
|
449
|
+
const nativeCollateralPda = deriveNativeCollateralPda(side.warpProgram);
|
|
450
|
+
keys.push(
|
|
451
|
+
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false },
|
|
452
|
+
{ pubkey: nativeCollateralPda, isSigner: false, isWritable: true }
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const data = encodeTransferRemote(destinationDomainFor(toChain), recipient, amount);
|
|
457
|
+
|
|
458
|
+
return new TransactionInstruction({
|
|
459
|
+
programId: side.warpProgram,
|
|
460
|
+
keys,
|
|
461
|
+
data,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// ─── High-level: build full bridge instruction set ────────────────
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Build all instructions needed to bridge tokens cross-chain with in-tx fee.
|
|
469
|
+
* Returns the instructions, the unique-message keypair (must sign the tx),
|
|
470
|
+
* and the fee/bridge amounts.
|
|
471
|
+
*/
|
|
472
|
+
export function makeBridgeIxs(params: BridgeTransferParams): BridgeIxsResult {
|
|
473
|
+
const {
|
|
474
|
+
token,
|
|
475
|
+
fromChain,
|
|
476
|
+
sender,
|
|
477
|
+
recipient,
|
|
478
|
+
amount,
|
|
479
|
+
feeBps,
|
|
480
|
+
feeRecipient,
|
|
481
|
+
skipFee,
|
|
482
|
+
} = params;
|
|
483
|
+
|
|
484
|
+
if (amount <= 0n) throw new Error("amount must be > 0");
|
|
485
|
+
|
|
486
|
+
const split = skipFee
|
|
487
|
+
? { feeAmount: 0n, bridgeAmount: amount, feeBps: 0 }
|
|
488
|
+
: calculateBridgeFee(amount, feeBps);
|
|
489
|
+
|
|
490
|
+
if (split.bridgeAmount <= 0n) {
|
|
491
|
+
throw new Error("bridge amount after fee is zero — increase amount or lower feeBps");
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const recipientForFee = feeRecipient ?? getBridgeFeeRecipient();
|
|
495
|
+
const feeIxs = makeBridgeFeeIxs({
|
|
496
|
+
token,
|
|
497
|
+
fromChain,
|
|
498
|
+
sender,
|
|
499
|
+
feeRecipient: recipientForFee,
|
|
500
|
+
feeAmount: split.feeAmount,
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
const uniqueMessageKeypair = Keypair.generate();
|
|
504
|
+
const transferIx = makeTransferRemoteIx({
|
|
505
|
+
token,
|
|
506
|
+
fromChain,
|
|
507
|
+
sender,
|
|
508
|
+
recipient,
|
|
509
|
+
amount: split.bridgeAmount,
|
|
510
|
+
uniqueMessageKeypair,
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
return {
|
|
514
|
+
instructions: [...feeIxs, transferIx],
|
|
515
|
+
uniqueMessageKeypair,
|
|
516
|
+
feeAmount: split.feeAmount,
|
|
517
|
+
bridgeAmount: split.bridgeAmount,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// ─── Send + confirm ───────────────────────────────────────────────
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Build, sign, and send a bridge transfer in one call.
|
|
525
|
+
* `wallet` is the sender keypair on the source chain and pays fees + signs.
|
|
526
|
+
*
|
|
527
|
+
* `connection` must point to the SOURCE chain RPC:
|
|
528
|
+
* - fromChain: "solana" → Solana mainnet RPC
|
|
529
|
+
* - fromChain: "nara" → Nara mainnet RPC
|
|
530
|
+
*/
|
|
531
|
+
export async function bridgeTransfer(
|
|
532
|
+
connection: Connection,
|
|
533
|
+
wallet: Keypair,
|
|
534
|
+
params: Omit<BridgeTransferParams, "sender">,
|
|
535
|
+
opts?: { skipPreflight?: boolean; computeUnitLimit?: number; computeUnitPrice?: number | "auto" }
|
|
536
|
+
): Promise<BridgeTransferResult> {
|
|
537
|
+
const built = makeBridgeIxs({
|
|
538
|
+
...params,
|
|
539
|
+
sender: wallet.publicKey,
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
const signature = await sendTx(
|
|
543
|
+
connection,
|
|
544
|
+
wallet,
|
|
545
|
+
built.instructions,
|
|
546
|
+
[built.uniqueMessageKeypair],
|
|
547
|
+
{
|
|
548
|
+
computeUnitLimit: opts?.computeUnitLimit ?? 1_400_000,
|
|
549
|
+
computeUnitPrice: opts?.computeUnitPrice,
|
|
550
|
+
skipPreflight: opts?.skipPreflight,
|
|
551
|
+
}
|
|
552
|
+
);
|
|
553
|
+
|
|
554
|
+
// Try to extract message_id from logs
|
|
555
|
+
const messageId = await extractMessageId(connection, signature);
|
|
556
|
+
|
|
557
|
+
return {
|
|
558
|
+
signature,
|
|
559
|
+
messageId,
|
|
560
|
+
feeAmount: built.feeAmount,
|
|
561
|
+
bridgeAmount: built.bridgeAmount,
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// ─── Message ID extraction ────────────────────────────────────────
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Extract the cross-chain message_id from a dispatch tx's logs.
|
|
569
|
+
* Returns null if the log is missing (tx not yet visible to RPC, etc.).
|
|
570
|
+
*/
|
|
571
|
+
export async function extractMessageId(
|
|
572
|
+
connection: Connection,
|
|
573
|
+
signature: string
|
|
574
|
+
): Promise<string | null> {
|
|
575
|
+
const tx = await connection.getTransaction(signature, {
|
|
576
|
+
maxSupportedTransactionVersion: 0,
|
|
577
|
+
commitment: "confirmed",
|
|
578
|
+
});
|
|
579
|
+
if (!tx?.meta?.logMessages) return null;
|
|
580
|
+
for (const log of tx.meta.logMessages) {
|
|
581
|
+
const m = log.match(/Dispatched message to \d+, ID (0x[0-9a-fA-F]+)/);
|
|
582
|
+
if (m && m[1]) return m[1];
|
|
583
|
+
}
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// ─── Message delivery status ──────────────────────────────────────
|
|
588
|
+
|
|
589
|
+
export interface MessageStatus {
|
|
590
|
+
/** Whether the message has been processed on the destination chain */
|
|
591
|
+
delivered: boolean;
|
|
592
|
+
/** Destination chain tx signature (null if not yet delivered) */
|
|
593
|
+
deliverySignature: string | null;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* Query whether a cross-chain message has been delivered on the destination chain.
|
|
598
|
+
*
|
|
599
|
+
* Scans recent transactions on the destination Mailbox for the message_id.
|
|
600
|
+
* The Hyperlane inbox emits: "Hyperlane inbox processed message 0x..."
|
|
601
|
+
*
|
|
602
|
+
* @param destConnection - connection to the DESTINATION chain RPC
|
|
603
|
+
* @param messageId - the 0x-prefixed message ID from extractMessageId()
|
|
604
|
+
* @param toChain - "solana" | "nara" (which chain to check)
|
|
605
|
+
* @param opts.limit - how many recent Mailbox txs to scan (default 50)
|
|
606
|
+
*/
|
|
607
|
+
export async function queryMessageStatus(
|
|
608
|
+
destConnection: Connection,
|
|
609
|
+
messageId: string,
|
|
610
|
+
toChain: BridgeChain,
|
|
611
|
+
opts?: { limit?: number }
|
|
612
|
+
): Promise<MessageStatus> {
|
|
613
|
+
const mailbox = mailboxFor(toChain);
|
|
614
|
+
const limit = opts?.limit ?? 50;
|
|
615
|
+
|
|
616
|
+
const sigs = await destConnection.getSignaturesForAddress(mailbox, {
|
|
617
|
+
limit,
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
for (const entry of sigs) {
|
|
621
|
+
if (entry.err) continue;
|
|
622
|
+
const tx = await destConnection.getTransaction(entry.signature, {
|
|
623
|
+
maxSupportedTransactionVersion: 0,
|
|
624
|
+
commitment: "confirmed",
|
|
625
|
+
});
|
|
626
|
+
if (!tx?.meta?.logMessages) continue;
|
|
627
|
+
for (const log of tx.meta.logMessages) {
|
|
628
|
+
if (log.includes(messageId)) {
|
|
629
|
+
return { delivered: true, deliverySignature: entry.signature };
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return { delivered: false, deliverySignature: null };
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// ─── Validator signature status (S3) ──────────────────────────────
|
|
638
|
+
|
|
639
|
+
const S3_BASE = "https://nara-hyperlane.s3.us-west-1.amazonaws.com";
|
|
640
|
+
|
|
641
|
+
/** Validator S3 prefixes per source chain */
|
|
642
|
+
const VALIDATOR_PREFIXES: Record<BridgeChain, string[]> = {
|
|
643
|
+
solana: ["validator-solana-1", "validator-solana-2", "validator-solana-3"],
|
|
644
|
+
nara: ["validator-nara-1", "validator-nara-2", "validator-nara-3"],
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
export interface ValidatorSignature {
|
|
648
|
+
folder: string;
|
|
649
|
+
signed: boolean;
|
|
650
|
+
latestIndex: number;
|
|
651
|
+
serializedSignature: string | null;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
export interface MessageSignatureStatus {
|
|
655
|
+
messageId: string;
|
|
656
|
+
/** Merkle tree checkpoint index for this message (null if not found) */
|
|
657
|
+
checkpointIndex: number | null;
|
|
658
|
+
sourceChain: BridgeChain;
|
|
659
|
+
validators: ValidatorSignature[];
|
|
660
|
+
signedCount: number;
|
|
661
|
+
totalValidators: number;
|
|
662
|
+
/** All validators have signed */
|
|
663
|
+
fullySigned: boolean;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
async function s3Json<T = unknown>(key: string): Promise<T | null> {
|
|
667
|
+
try {
|
|
668
|
+
const resp = await fetch(`${S3_BASE}/${key}`);
|
|
669
|
+
if (!resp.ok) return null;
|
|
670
|
+
return (await resp.json()) as T;
|
|
671
|
+
} catch {
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
interface S3Checkpoint {
|
|
677
|
+
value: {
|
|
678
|
+
checkpoint: {
|
|
679
|
+
merkle_tree_hook_address: string;
|
|
680
|
+
mailbox_domain: number;
|
|
681
|
+
root: string;
|
|
682
|
+
index: number;
|
|
683
|
+
};
|
|
684
|
+
message_id: string;
|
|
685
|
+
};
|
|
686
|
+
signature: { r: string; s: string; v: number };
|
|
687
|
+
serialized_signature: string;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Query Hyperlane validator signatures for a cross-chain message from AWS S3.
|
|
692
|
+
*
|
|
693
|
+
* Each validator stores signed merkle checkpoints in S3. Each checkpoint
|
|
694
|
+
* at index N contains the message_id of the Nth dispatched message and the
|
|
695
|
+
* validator's ECDSA signature over the merkle root.
|
|
696
|
+
*
|
|
697
|
+
* All validators are scanned concurrently (one goroutine per validator).
|
|
698
|
+
*
|
|
699
|
+
* @param messageId - 0x-prefixed message ID from extractMessageId()
|
|
700
|
+
* @param sourceChain - source chain where the message was dispatched
|
|
701
|
+
* @param opts.maxScan - how many checkpoints to scan backwards (default 200)
|
|
702
|
+
*/
|
|
703
|
+
export async function queryMessageSignatures(
|
|
704
|
+
messageId: string,
|
|
705
|
+
sourceChain: BridgeChain,
|
|
706
|
+
opts?: { maxScan?: number }
|
|
707
|
+
): Promise<MessageSignatureStatus> {
|
|
708
|
+
const prefixes = VALIDATOR_PREFIXES[sourceChain];
|
|
709
|
+
const maxScan = opts?.maxScan ?? 200;
|
|
710
|
+
|
|
711
|
+
// Each validator independently: fetch latest index → reverse-scan for messageId
|
|
712
|
+
const results = await Promise.all(
|
|
713
|
+
prefixes.map((folder) => scanValidator(folder, messageId, maxScan))
|
|
714
|
+
);
|
|
715
|
+
|
|
716
|
+
// checkpointIndex comes from whichever validator found it
|
|
717
|
+
const found = results.find((r) => r.checkpointIndex !== null);
|
|
718
|
+
const checkpointIndex = found?.checkpointIndex ?? null;
|
|
719
|
+
|
|
720
|
+
const validators: ValidatorSignature[] = results.map((r) => ({
|
|
721
|
+
folder: r.folder,
|
|
722
|
+
signed: r.signed,
|
|
723
|
+
latestIndex: r.latestIndex,
|
|
724
|
+
serializedSignature: r.serializedSignature,
|
|
725
|
+
}));
|
|
726
|
+
|
|
727
|
+
const signedCount = validators.filter((v) => v.signed).length;
|
|
728
|
+
return {
|
|
729
|
+
messageId,
|
|
730
|
+
checkpointIndex,
|
|
731
|
+
sourceChain,
|
|
732
|
+
validators,
|
|
733
|
+
signedCount,
|
|
734
|
+
totalValidators: prefixes.length,
|
|
735
|
+
fullySigned: signedCount === prefixes.length,
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
async function scanValidator(
|
|
740
|
+
folder: string,
|
|
741
|
+
messageId: string,
|
|
742
|
+
maxScan: number
|
|
743
|
+
): Promise<ValidatorSignature & { checkpointIndex: number | null }> {
|
|
744
|
+
const latestIndex = await s3Json<number>(
|
|
745
|
+
`${folder}/checkpoint_latest_index.json`
|
|
746
|
+
);
|
|
747
|
+
if (latestIndex === null) {
|
|
748
|
+
return {
|
|
749
|
+
folder,
|
|
750
|
+
signed: false,
|
|
751
|
+
latestIndex: -1,
|
|
752
|
+
serializedSignature: null,
|
|
753
|
+
checkpointIndex: null,
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const minIndex = Math.max(0, latestIndex - maxScan);
|
|
758
|
+
for (let i = latestIndex; i >= minIndex; i--) {
|
|
759
|
+
const cp = await s3Json<S3Checkpoint>(
|
|
760
|
+
`${folder}/checkpoint_${i}_with_id.json`
|
|
761
|
+
);
|
|
762
|
+
if (cp?.value?.message_id === messageId) {
|
|
763
|
+
return {
|
|
764
|
+
folder,
|
|
765
|
+
signed: true,
|
|
766
|
+
latestIndex,
|
|
767
|
+
serializedSignature: cp.serialized_signature,
|
|
768
|
+
checkpointIndex: i,
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
return {
|
|
774
|
+
folder,
|
|
775
|
+
signed: false,
|
|
776
|
+
latestIndex,
|
|
777
|
+
serializedSignature: null,
|
|
778
|
+
checkpointIndex: null,
|
|
779
|
+
};
|
|
780
|
+
}
|
package/src/constants.ts
CHANGED
|
@@ -45,3 +45,23 @@ export const DEFAULT_AGENT_REGISTRY_PROGRAM_ID =
|
|
|
45
45
|
* When empty, uses legacy transactions.
|
|
46
46
|
*/
|
|
47
47
|
export const DEFAULT_ALT_ADDRESS = process.env.ALT_ADDRESS || "3uw7RatGTB4hdHnuVLXjsqcMZ87zXsMSc3XbyoPA8mB7";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Bridge fee in basis points (1 bps = 0.01%). 50 = 0.5%.
|
|
51
|
+
* Deducted from the bridged amount and transferred to DEFAULT_BRIDGE_FEE_RECIPIENT
|
|
52
|
+
* in the same transaction.
|
|
53
|
+
*/
|
|
54
|
+
export const DEFAULT_BRIDGE_FEE_BPS = 50;
|
|
55
|
+
|
|
56
|
+
/** BPS denominator (10000 = 100%) */
|
|
57
|
+
export const BRIDGE_FEE_BPS_DENOMINATOR = 10000;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Default fee recipient pubkey for cross-chain bridge transactions.
|
|
61
|
+
* Same Ed25519 keypair works on both Solana and Nara chains.
|
|
62
|
+
* Override at runtime via setBridgeFeeRecipient() in src/bridge.ts.
|
|
63
|
+
*
|
|
64
|
+
* NOTE: replace with actual fee recipient before mainnet usage.
|
|
65
|
+
*/
|
|
66
|
+
export const DEFAULT_BRIDGE_FEE_RECIPIENT =
|
|
67
|
+
"FERLFwBpCyoEuvFP68eP6Fv4FCVocnNyyFUCYwpfmjqn";
|