@elizaos/plugin-wallet 2.0.0-beta.1

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.
Files changed (200) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +64 -0
  3. package/auto-enable.ts +76 -0
  4. package/dist/LpManagementService-BWrQ5-cO.mjs +353 -0
  5. package/dist/MockLpService-D_Apn4Fd.mjs +99 -0
  6. package/dist/aerodrome-CfnESC32.mjs +890 -0
  7. package/dist/chunk-hT5z_Zn9.mjs +35 -0
  8. package/dist/index.d.mts +34727 -0
  9. package/dist/index.mjs +21590 -0
  10. package/dist/lib/server-wallet-trade.d.mts +34 -0
  11. package/dist/lib/server-wallet-trade.mjs +306 -0
  12. package/dist/meteora-BPX39hZo.mjs +22640 -0
  13. package/dist/orca-Bybp1HXO.mjs +249 -0
  14. package/dist/pancakeswp-CkEXlXti.mjs +604 -0
  15. package/dist/plugin-ZO_MTyd0.mjs +529 -0
  16. package/dist/raydium-rfaM9yEf.mjs +539 -0
  17. package/dist/sdk/index.d.mts +32492 -0
  18. package/dist/sdk/index.mjs +6415 -0
  19. package/dist/types-D5252NZk.mjs +487 -0
  20. package/dist/uniswap-CReXgXVN.mjs +573 -0
  21. package/dist/wallet-action.d.mts +6 -0
  22. package/dist/wallet-action.mjs +820 -0
  23. package/package.json +152 -0
  24. package/src/actions/failure-codes.ts +79 -0
  25. package/src/actions/index.ts +1 -0
  26. package/src/analytics/birdeye/actions/wallet-search-address.ts +9 -0
  27. package/src/analytics/birdeye/birdeye-task.ts +175 -0
  28. package/src/analytics/birdeye/birdeye.ts +813 -0
  29. package/src/analytics/birdeye/constants.ts +74 -0
  30. package/src/analytics/birdeye/providers/agent-portfolio-provider.ts +18 -0
  31. package/src/analytics/birdeye/providers/market.ts +227 -0
  32. package/src/analytics/birdeye/providers/portfolio-factory.test.ts +138 -0
  33. package/src/analytics/birdeye/providers/portfolio-factory.ts +252 -0
  34. package/src/analytics/birdeye/providers/trending.ts +365 -0
  35. package/src/analytics/birdeye/providers/wallet.ts +14 -0
  36. package/src/analytics/birdeye/search-category.test.ts +207 -0
  37. package/src/analytics/birdeye/search-category.ts +506 -0
  38. package/src/analytics/birdeye/service.ts +992 -0
  39. package/src/analytics/birdeye/tasks/birdeye.ts +232 -0
  40. package/src/analytics/birdeye/types/api/common.ts +305 -0
  41. package/src/analytics/birdeye/types/api/defi.ts +220 -0
  42. package/src/analytics/birdeye/types/api/pair.ts +200 -0
  43. package/src/analytics/birdeye/types/api/search.ts +86 -0
  44. package/src/analytics/birdeye/types/api/token.ts +635 -0
  45. package/src/analytics/birdeye/types/api/trader.ts +76 -0
  46. package/src/analytics/birdeye/types/api/wallet.ts +181 -0
  47. package/src/analytics/birdeye/types/shared.ts +106 -0
  48. package/src/analytics/birdeye/utils.ts +700 -0
  49. package/src/analytics/dexscreener/errors.ts +28 -0
  50. package/src/analytics/dexscreener/index.ts +3 -0
  51. package/src/analytics/dexscreener/search-category.test.ts +49 -0
  52. package/src/analytics/dexscreener/search-category.ts +42 -0
  53. package/src/analytics/dexscreener/service.ts +595 -0
  54. package/src/analytics/dexscreener/types.ts +128 -0
  55. package/src/analytics/lpinfo/index.d.ts +7 -0
  56. package/src/analytics/lpinfo/index.ts +52 -0
  57. package/src/analytics/lpinfo/kamino/README.md +102 -0
  58. package/src/analytics/lpinfo/kamino/index.ts +24 -0
  59. package/src/analytics/lpinfo/kamino/providers/kaminoLiquidityProvider.ts +422 -0
  60. package/src/analytics/lpinfo/kamino/providers/kaminoPoolProvider.ts +365 -0
  61. package/src/analytics/lpinfo/kamino/providers/kaminoProvider.ts +496 -0
  62. package/src/analytics/lpinfo/kamino/services/kaminoLiquidityService.ts +1123 -0
  63. package/src/analytics/lpinfo/kamino/services/kaminoService.ts +758 -0
  64. package/src/analytics/lpinfo/steer/README.md +169 -0
  65. package/src/analytics/lpinfo/steer/index.ts +23 -0
  66. package/src/analytics/lpinfo/steer/providers/steerLiquidityProvider.ts +544 -0
  67. package/src/analytics/lpinfo/steer/services/steerLiquidityService.ts +1690 -0
  68. package/src/analytics/lpinfo/steer/steer-display-types.ts +99 -0
  69. package/src/analytics/news/index.ts +52 -0
  70. package/src/analytics/news/interfaces/types.ts +222 -0
  71. package/src/analytics/news/providers/defiNewsProvider.ts +734 -0
  72. package/src/analytics/news/services/newsDataService.ts +332 -0
  73. package/src/analytics/news/utils/formatters.ts +151 -0
  74. package/src/analytics/token-info/action.ts +240 -0
  75. package/src/analytics/token-info/index.ts +3 -0
  76. package/src/analytics/token-info/params.ts +215 -0
  77. package/src/analytics/token-info/providers.ts +681 -0
  78. package/src/analytics/token-info/service.ts +168 -0
  79. package/src/analytics/token-info/types.ts +74 -0
  80. package/src/audit/audit-log.ts +45 -0
  81. package/src/browser-shim/build-shim.ts +123 -0
  82. package/src/browser-shim/index.ts +5 -0
  83. package/src/browser-shim/shim.template.js +563 -0
  84. package/src/chains/evm/.github/workflows/npm-deploy.yml +112 -0
  85. package/src/chains/evm/LICENSE +21 -0
  86. package/src/chains/evm/README.md +106 -0
  87. package/src/chains/evm/actions/helpers.ts +147 -0
  88. package/src/chains/evm/actions/swap.ts +839 -0
  89. package/src/chains/evm/actions/transfer.ts +254 -0
  90. package/src/chains/evm/biome.json +61 -0
  91. package/src/chains/evm/bridge-router.ts +660 -0
  92. package/src/chains/evm/build.ts +89 -0
  93. package/src/chains/evm/chain-handler.ts +416 -0
  94. package/src/chains/evm/constants.ts +23 -0
  95. package/src/chains/evm/contracts/artifacts/OZGovernor.json +1707 -0
  96. package/src/chains/evm/contracts/artifacts/TimelockController.json +1007 -0
  97. package/src/chains/evm/contracts/artifacts/VoteToken.json +895 -0
  98. package/src/chains/evm/dex/aerodrome/index.ts +34 -0
  99. package/src/chains/evm/dex/aerodrome/services/AerodromeLpService.ts +558 -0
  100. package/src/chains/evm/dex/aerodrome/types.ts +318 -0
  101. package/src/chains/evm/dex/pancakeswp/index.ts +35 -0
  102. package/src/chains/evm/dex/pancakeswp/services/PancakeSwapV3LpService.ts +743 -0
  103. package/src/chains/evm/dex/pancakeswp/types.ts +65 -0
  104. package/src/chains/evm/dex/uniswap/index.ts +35 -0
  105. package/src/chains/evm/dex/uniswap/services/UniswapV3LpService.ts +759 -0
  106. package/src/chains/evm/dex/uniswap/types.ts +390 -0
  107. package/src/chains/evm/generated/specs/spec-helpers.ts +73 -0
  108. package/src/chains/evm/generated/specs/specs.ts +151 -0
  109. package/src/chains/evm/gov-router.ts +250 -0
  110. package/src/chains/evm/index.browser.ts +16 -0
  111. package/src/chains/evm/index.ts +31 -0
  112. package/src/chains/evm/prompts.ts +193 -0
  113. package/src/chains/evm/providers/get-balance.ts +123 -0
  114. package/src/chains/evm/providers/wallet.ts +715 -0
  115. package/src/chains/evm/routes/sign.ts +333 -0
  116. package/src/chains/evm/rpc-providers.ts +410 -0
  117. package/src/chains/evm/service.ts +140 -0
  118. package/src/chains/evm/templates/index.ts +10 -0
  119. package/src/chains/evm/types/index.ts +432 -0
  120. package/src/chains/evm/vitest.config.ts +18 -0
  121. package/src/chains/registry.ts +668 -0
  122. package/src/chains/solana/README.md +367 -0
  123. package/src/chains/wallet-action.ts +533 -0
  124. package/src/chains/wallet-router.test.ts +296 -0
  125. package/src/contracts.ts +65 -0
  126. package/src/core-augmentation.ts +10 -0
  127. package/src/index.ts +71 -0
  128. package/src/lib/server-wallet-trade.ts +192 -0
  129. package/src/lib/wallet-export-guard.ts +330 -0
  130. package/src/lp/actions/liquidity.ts +827 -0
  131. package/src/lp/e2e/real-token-tests.ts +428 -0
  132. package/src/lp/e2e/scenarios.ts +470 -0
  133. package/src/lp/e2e/test-utils.ts +145 -0
  134. package/src/lp/lp-manager-entry.ts +303 -0
  135. package/src/lp/services/ConcentratedLiquidityService.ts +120 -0
  136. package/src/lp/services/DexInteractionService.ts +226 -0
  137. package/src/lp/services/LpManagementService.test.ts +148 -0
  138. package/src/lp/services/LpManagementService.ts +632 -0
  139. package/src/lp/services/UserLpProfileService.ts +163 -0
  140. package/src/lp/services/VaultService.ts +153 -0
  141. package/src/lp/services/YieldOptimizationService.ts +344 -0
  142. package/src/lp/services/__tests__/MockLpService.ts +146 -0
  143. package/src/lp/tasks/LpAutoRebalanceTask.ts +117 -0
  144. package/src/lp/tasks/__tests__/LpAutoRebalanceTask.test.ts +370 -0
  145. package/src/lp/types.ts +582 -0
  146. package/src/lp/utils/solanaClient.ts +143 -0
  147. package/src/plugin.ts +125 -0
  148. package/src/policy/policy.ts +19 -0
  149. package/src/providers/canonical-provider.ts +27 -0
  150. package/src/providers/unified-wallet-provider.ts +79 -0
  151. package/src/register-routes.ts +11 -0
  152. package/src/routes/plugin.ts +47 -0
  153. package/src/routes/wallet-market-overview-route.ts +869 -0
  154. package/src/sdk/abi.ts +258 -0
  155. package/src/sdk/bridge/abis.ts +126 -0
  156. package/src/sdk/bridge/client.ts +518 -0
  157. package/src/sdk/bridge/index.ts +56 -0
  158. package/src/sdk/bridge/solana.ts +604 -0
  159. package/src/sdk/bridge/types.ts +202 -0
  160. package/src/sdk/convenience.ts +347 -0
  161. package/src/sdk/escrow/MutualStakeEscrow.ts +480 -0
  162. package/src/sdk/escrow/types.ts +64 -0
  163. package/src/sdk/escrow/verifiers.ts +73 -0
  164. package/src/sdk/identity/erc8004.ts +692 -0
  165. package/src/sdk/identity/reputation.ts +449 -0
  166. package/src/sdk/identity/uaid.ts +497 -0
  167. package/src/sdk/identity/validation.ts +372 -0
  168. package/src/sdk/index.ts +763 -0
  169. package/src/sdk/policy/SpendingPolicy.ts +260 -0
  170. package/src/sdk/policy/UptoBillingPolicy.ts +320 -0
  171. package/src/sdk/router/PaymentRouter.ts +215 -0
  172. package/src/sdk/router/index.ts +8 -0
  173. package/src/sdk/swap/SwapModule.ts +310 -0
  174. package/src/sdk/swap/abi.ts +117 -0
  175. package/src/sdk/swap/index.ts +34 -0
  176. package/src/sdk/swap/types.ts +135 -0
  177. package/src/sdk/tokens/decimals.ts +140 -0
  178. package/src/sdk/tokens/registry.ts +911 -0
  179. package/src/sdk/tokens/solana.ts +419 -0
  180. package/src/sdk/tokens/transfers.ts +327 -0
  181. package/src/sdk/types.ts +158 -0
  182. package/src/sdk/wallet-core.ts +115 -0
  183. package/src/sdk/x402/budget.ts +168 -0
  184. package/src/sdk/x402/chains/abstract/index.ts +280 -0
  185. package/src/sdk/x402/client.ts +320 -0
  186. package/src/sdk/x402/index.ts +46 -0
  187. package/src/sdk/x402/middleware.ts +92 -0
  188. package/src/sdk/x402/multi-asset.ts +144 -0
  189. package/src/sdk/x402/types.ts +156 -0
  190. package/src/services/wallet-backend-service.ts +328 -0
  191. package/src/types/wallet-router.ts +227 -0
  192. package/src/utils/intent-trajectory.ts +106 -0
  193. package/src/wallet/backend.ts +62 -0
  194. package/src/wallet/errors.ts +49 -0
  195. package/src/wallet/index.ts +27 -0
  196. package/src/wallet/local-eoa-backend.ts +201 -0
  197. package/src/wallet/pending.ts +60 -0
  198. package/src/wallet/select-backend.ts +47 -0
  199. package/src/wallet/steward-backend.ts +161 -0
  200. package/src/wallet-action.ts +1 -0
@@ -0,0 +1,604 @@
1
+ /**
2
+ * SolanaBridgeModule — CCTP V2 cross-chain USDC bridge between EVM chains and Solana.
3
+ *
4
+ * Uses Circle's Cross-Chain Transfer Protocol (CCTP) V2 for trustless USDC transfers.
5
+ * EVM side uses viem; Solana side uses @solana/web3.js as an optional peer dependency.
6
+ *
7
+ * Sources:
8
+ * - Circle CCTP Solana docs: https://developers.circle.com/cctp/docs/solana
9
+ * - Solana USDC mint: https://solana.com/ecosystem/usdc
10
+ * - CCTP domain IDs: https://developers.circle.com/cctp/references/contract-addresses
11
+ */
12
+
13
+ import {
14
+ type Address,
15
+ type Chain,
16
+ createPublicClient,
17
+ getContract,
18
+ type Hash,
19
+ type Hex,
20
+ http,
21
+ keccak256,
22
+ type PublicClient,
23
+ pad,
24
+ type WalletClient,
25
+ } from "viem";
26
+ import {
27
+ arbitrum,
28
+ avalanche,
29
+ base,
30
+ linea,
31
+ mainnet,
32
+ optimism,
33
+ polygon,
34
+ } from "viem/chains";
35
+ import {
36
+ ERC20BridgeAbi,
37
+ MessageTransmitterV2Abi,
38
+ TokenMessengerV2Abi,
39
+ } from "./abis.js";
40
+ import {
41
+ ATTESTATION_POLL_INTERVAL_MS,
42
+ CCTP_DOMAIN_IDS,
43
+ CIRCLE_ATTESTATION_API,
44
+ type EVMBridgeChain,
45
+ FINALITY_THRESHOLD,
46
+ MAX_ATTESTATION_POLLS,
47
+ MESSAGE_TRANSMITTER_V2 as MESSAGE_TRANSMITTER_V2_MAP,
48
+ TOKEN_MESSENGER_V2,
49
+ USDC_CONTRACT,
50
+ } from "./types.js";
51
+
52
+ // ─── Solana Constants ───
53
+
54
+ /** Solana CCTP domain ID */
55
+ export const SOLANA_CCTP_DOMAIN = 5;
56
+
57
+ /** Native USDC mint on Solana Mainnet */
58
+ export const SOLANA_USDC_MINT =
59
+ "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v" as const;
60
+
61
+ /**
62
+ * Solana CCTP V2 TokenMessengerMinterV2 program address.
63
+ * This is the V2 program (not V1 CCTPiPYP...). The V2 program combines
64
+ * TokenMessengerV2 + TokenMinterV2 into a single program.
65
+ * Source: https://developers.circle.com/cctp/references/solana-programs
66
+ * Verified: https://github.com/circlefin/solana-cctp-contracts (programs/v2)
67
+ */
68
+ export const SOLANA_TOKEN_MESSENGER =
69
+ "CCTPV2vPZJS2u2BBsUoscuikbYjnpFmbFsvVuJdgUMQe" as const;
70
+
71
+ /**
72
+ * Solana CCTP V2 MessageTransmitterV2 program address.
73
+ * This is the V2 program (not V1 CCTPmbSD...).
74
+ * Source: https://developers.circle.com/cctp/references/solana-programs
75
+ * Verified: https://github.com/circlefin/solana-cctp-contracts (programs/v2)
76
+ */
77
+ export const SOLANA_MESSAGE_TRANSMITTER =
78
+ "CCTPV2Sm4AdWt5296sk4P66VBZ7bEhcARwFaaS9YPbeC" as const;
79
+
80
+ /** Default Solana Mainnet RPC endpoint */
81
+ export const SOLANA_DEFAULT_RPC =
82
+ "https://api.mainnet-beta.solana.com" as const;
83
+
84
+ // ─── Viem chain definitions for EVM side ───
85
+
86
+ const VIEM_CHAINS: Record<EVMBridgeChain, Chain> = {
87
+ base,
88
+ ethereum: mainnet,
89
+ optimism,
90
+ arbitrum,
91
+ polygon,
92
+ avalanche,
93
+ linea,
94
+ unichain: {
95
+ id: 130,
96
+ name: "Unichain",
97
+ nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 },
98
+ rpcUrls: { default: { http: ["https://mainnet.unichain.org"] } },
99
+ },
100
+ sonic: {
101
+ id: 146,
102
+ name: "Sonic",
103
+ nativeCurrency: { name: "S", symbol: "S", decimals: 18 },
104
+ rpcUrls: { default: { http: ["https://rpc.soniclabs.com"] } },
105
+ },
106
+ worldchain: {
107
+ id: 480,
108
+ name: "World Chain",
109
+ nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 },
110
+ rpcUrls: {
111
+ default: { http: ["https://worldchain-mainnet.g.alchemy.com/public"] },
112
+ },
113
+ },
114
+ };
115
+
116
+ // ─── Helpers ───
117
+
118
+ /**
119
+ * Encode a Solana base58 public key as a 32-byte hex buffer for CCTP.
120
+ * CCTP requires the mint recipient to be a 32-byte zero-padded value.
121
+ * For Solana, this is the base58-decoded public key (already 32 bytes).
122
+ */
123
+ function solanaPubkeyToBytes32(base58Address: string): Hex {
124
+ // Dynamic import for optional @solana/web3.js
125
+ // We manually decode base58 here to avoid requiring the dependency at import time.
126
+ const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
127
+ const bytes = new Uint8Array(32);
128
+ let intVal = 0n;
129
+ for (const char of base58Address) {
130
+ const digit = BigInt(ALPHABET.indexOf(char));
131
+ if (digit < 0n)
132
+ throw new Error(
133
+ `SolanaBridge: Invalid base58 character '${char}' in address.`,
134
+ );
135
+ intVal = intVal * 58n + digit;
136
+ }
137
+ // Write to 32-byte big-endian buffer
138
+ for (let i = 31; i >= 0; i--) {
139
+ bytes[i] = Number(intVal & 0xffn);
140
+ intVal >>= 8n;
141
+ }
142
+ if (intVal !== 0n)
143
+ throw new Error(`SolanaBridge: Address value overflows 32 bytes.`);
144
+ const hex = Array.from(bytes)
145
+ .map((b) => b.toString(16).padStart(2, "0"))
146
+ .join("");
147
+ return `0x${hex}` as Hex;
148
+ }
149
+
150
+ /**
151
+ * Decode a 32-byte hex buffer back to a Solana base58 public key.
152
+ * Used when interpreting Solana→EVM message recipient fields.
153
+ */
154
+ export function bytes32ToSolanaPubkey(bytes32: Hex): string {
155
+ const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
156
+ const hex = bytes32.startsWith("0x") ? bytes32.slice(2) : bytes32;
157
+ if (hex.length !== 64)
158
+ throw new Error(
159
+ `SolanaBridge: Expected 32-byte hex (64 chars), got ${hex.length}.`,
160
+ );
161
+ let intVal = BigInt(`0x${hex}`);
162
+ let result = "";
163
+ while (intVal > 0n) {
164
+ const rem = Number(intVal % 58n);
165
+ intVal /= 58n;
166
+ result = ALPHABET[rem] + result;
167
+ }
168
+ return result || "1";
169
+ }
170
+
171
+ // ─── EVM→Solana Bridge ───
172
+
173
+ export interface EVMToSolanaOptions {
174
+ /** Solana recipient address (base58). Defaults to the EVM signer's address re-encoded — usually wrong; always specify. */
175
+ solanaRecipient: string;
176
+ /** EVM source chain. Defaults to 'base'. */
177
+ fromChain?: EVMBridgeChain;
178
+ /** Optional EVM RPC URL override. */
179
+ evmRpcUrl?: string;
180
+ /** Finality threshold: FAST (0) or FINALIZED (1000). Default: FAST. */
181
+ minFinalityThreshold?: number;
182
+ /** Max bridge fee in USDC base units. Default: 0 (no fee). */
183
+ maxFee?: bigint;
184
+ /** Circle IRIS API URL override. */
185
+ attestationApiUrl?: string;
186
+ }
187
+
188
+ export interface EVMToSolanaResult {
189
+ /** EVM burn transaction hash */
190
+ burnTxHash: Hash;
191
+ /** Circle message hash (used to poll for attestation) */
192
+ messageHash: Hex;
193
+ /** Raw CCTP message bytes (for manual receiveMessage on Solana) */
194
+ messageBytes: Hex;
195
+ /** Circle attestation signature (for receiveMessage on Solana) */
196
+ attestation: Hex;
197
+ /** The Solana CCTP domain (5) */
198
+ destinationDomain: number;
199
+ /** Amount bridged in USDC base units */
200
+ amount: bigint;
201
+ /** Elapsed time in milliseconds */
202
+ elapsedMs: number;
203
+ }
204
+
205
+ /**
206
+ * Bridge USDC from an EVM chain to Solana using CCTP V2.
207
+ *
208
+ * This function handles the EVM burn side and attestation polling.
209
+ * The Solana receive side must be completed by calling `receiveMessageOnSolana()`
210
+ * or by the Solana recipient manually submitting the message and attestation
211
+ * to the Solana MessageTransmitter program.
212
+ *
213
+ * @param walletClient - Viem WalletClient with a signer attached (for EVM)
214
+ * @param amount - Amount to bridge in USDC base units (6 decimals, e.g. 1_000_000n = 1 USDC)
215
+ * @param options - Bridge options including solanaRecipient (required)
216
+ * @returns Burn tx hash, attestation, and message bytes for Solana receive
217
+ */
218
+ export async function bridgeEVMToSolana(
219
+ walletClient: WalletClient,
220
+ amount: bigint,
221
+ options: EVMToSolanaOptions,
222
+ ): Promise<EVMToSolanaResult> {
223
+ const startMs = Date.now();
224
+ const fromChain = options.fromChain ?? "base";
225
+ const minFinalityThreshold =
226
+ options.minFinalityThreshold ?? FINALITY_THRESHOLD.FAST;
227
+ const maxFee = options.maxFee ?? 0n;
228
+ const attestationApiUrl = options.attestationApiUrl ?? CIRCLE_ATTESTATION_API;
229
+
230
+ if (!walletClient.account) {
231
+ throw new SolanaBridgeError(
232
+ "NO_WALLET_CLIENT",
233
+ "WalletClient must have an account attached.",
234
+ );
235
+ }
236
+ if (amount <= 0n) {
237
+ throw new SolanaBridgeError(
238
+ "INVALID_AMOUNT",
239
+ `Bridge amount must be > 0. Received: ${amount}.`,
240
+ );
241
+ }
242
+ if (!options.solanaRecipient) {
243
+ throw new SolanaBridgeError(
244
+ "INVALID_RECIPIENT",
245
+ "solanaRecipient (base58 Solana address) is required.",
246
+ );
247
+ }
248
+
249
+ // Encode Solana address as 32-byte CCTP mint recipient
250
+ const mintRecipient = solanaPubkeyToBytes32(options.solanaRecipient);
251
+
252
+ // Zero destinationCaller = any relayer can submit on Solana
253
+ const destinationCaller = pad("0x0" as Hex, { size: 32 });
254
+
255
+ const publicClient = createPublicClient({
256
+ chain: VIEM_CHAINS[fromChain],
257
+ transport: http(options.evmRpcUrl),
258
+ }) as PublicClient;
259
+
260
+ const account = walletClient.account;
261
+ const usdcAddress = USDC_CONTRACT[fromChain];
262
+ const messengerAddress = TOKEN_MESSENGER_V2[fromChain];
263
+
264
+ // Approve USDC if needed
265
+ const usdcRead = getContract({
266
+ address: usdcAddress,
267
+ abi: ERC20BridgeAbi,
268
+ client: publicClient,
269
+ });
270
+ const currentAllowance = (await usdcRead.read.allowance([
271
+ account.address,
272
+ messengerAddress,
273
+ ])) as bigint;
274
+ if (currentAllowance < amount) {
275
+ const usdcWrite = getContract({
276
+ address: usdcAddress,
277
+ abi: ERC20BridgeAbi,
278
+ client: { public: publicClient, wallet: walletClient },
279
+ });
280
+ const approveTxHash = await usdcWrite.write.approve(
281
+ [messengerAddress, amount],
282
+ {
283
+ account,
284
+ chain: VIEM_CHAINS[fromChain],
285
+ },
286
+ );
287
+ const approveReceipt = await publicClient.waitForTransactionReceipt({
288
+ hash: approveTxHash,
289
+ });
290
+ if (approveReceipt.status !== "success") {
291
+ throw new SolanaBridgeError(
292
+ "INSUFFICIENT_ALLOWANCE",
293
+ `USDC approve failed (tx: ${approveTxHash}).`,
294
+ );
295
+ }
296
+ }
297
+
298
+ // Burn USDC via CCTP V2 depositForBurn targeting Solana domain
299
+ const messenger = getContract({
300
+ address: messengerAddress,
301
+ abi: TokenMessengerV2Abi,
302
+ client: { public: publicClient, wallet: walletClient },
303
+ });
304
+
305
+ let burnTxHash: Hash;
306
+ try {
307
+ burnTxHash = await messenger.write.depositForBurn(
308
+ [
309
+ amount,
310
+ SOLANA_CCTP_DOMAIN,
311
+ mintRecipient,
312
+ usdcAddress,
313
+ destinationCaller,
314
+ maxFee,
315
+ minFinalityThreshold,
316
+ ],
317
+ { account, chain: VIEM_CHAINS[fromChain] },
318
+ );
319
+ } catch (err: unknown) {
320
+ const msg = err instanceof Error ? err.message : String(err);
321
+ throw new SolanaBridgeError(
322
+ "BURN_FAILED",
323
+ `CCTP depositForBurn to Solana failed: ${msg}.`,
324
+ );
325
+ }
326
+
327
+ const receipt = await publicClient.waitForTransactionReceipt({
328
+ hash: burnTxHash,
329
+ });
330
+ if (receipt.status !== "success") {
331
+ throw new SolanaBridgeError(
332
+ "BURN_FAILED",
333
+ `depositForBurn transaction reverted (tx: ${burnTxHash}).`,
334
+ );
335
+ }
336
+
337
+ // Extract message bytes from MessageSent event
338
+ const { messageBytes, messageHash } = extractMessageSent(receipt.logs);
339
+
340
+ // Poll Circle IRIS for attestation
341
+ const attestation = await pollForAttestation(
342
+ messageHash,
343
+ CCTP_DOMAIN_IDS[fromChain],
344
+ attestationApiUrl,
345
+ );
346
+
347
+ return {
348
+ burnTxHash,
349
+ messageHash,
350
+ messageBytes,
351
+ attestation,
352
+ destinationDomain: SOLANA_CCTP_DOMAIN,
353
+ amount,
354
+ elapsedMs: Date.now() - startMs,
355
+ };
356
+ }
357
+
358
+ // ─── Solana→EVM Bridge ───
359
+
360
+ export interface SolanaToEVMOptions {
361
+ /** EVM destination chain */
362
+ toChain: EVMBridgeChain;
363
+ /** Optional EVM destination RPC URL override */
364
+ evmRpcUrl?: string;
365
+ /** EVM recipient address. Defaults to the EVM walletClient's account address. */
366
+ evmRecipient?: Address;
367
+ /** Circle IRIS API URL override */
368
+ attestationApiUrl?: string;
369
+ /** Solana RPC URL override */
370
+ solanaRpcUrl?: string;
371
+ }
372
+
373
+ export interface SolanaToEVMBurnParams {
374
+ /** Raw CCTP message bytes from Solana burn transaction */
375
+ messageBytes: Hex;
376
+ /** Circle message hash */
377
+ messageHash: Hex;
378
+ /** Source domain (should be SOLANA_CCTP_DOMAIN = 5) */
379
+ sourceDomain: number;
380
+ }
381
+
382
+ export interface SolanaToEVMResult {
383
+ /** EVM mint transaction hash */
384
+ mintTxHash: Hash;
385
+ /** Amount received in USDC base units */
386
+ amount: bigint;
387
+ /** EVM destination chain */
388
+ toChain: EVMBridgeChain;
389
+ /** EVM recipient address */
390
+ recipient: Address;
391
+ /** Elapsed time in milliseconds */
392
+ elapsedMs: number;
393
+ }
394
+
395
+ /**
396
+ * Complete the Solana→EVM bridge by polling for Circle's attestation and minting on EVM.
397
+ *
398
+ * The caller is responsible for initiating the Solana burn via the Solana
399
+ * CCTP MessageTransmitter and passing the resulting messageBytes + messageHash here.
400
+ *
401
+ * @param walletClient - Viem WalletClient (for EVM signing)
402
+ * @param burnParams - Message bytes and hash from the Solana burn transaction
403
+ * @param options - Destination chain and optional overrides
404
+ */
405
+ export async function receiveFromSolanaOnEVM(
406
+ walletClient: WalletClient,
407
+ burnParams: SolanaToEVMBurnParams,
408
+ options: SolanaToEVMOptions,
409
+ ): Promise<SolanaToEVMResult> {
410
+ const startMs = Date.now();
411
+ const { toChain, evmRpcUrl } = options;
412
+ const attestationApiUrl = options.attestationApiUrl ?? CIRCLE_ATTESTATION_API;
413
+
414
+ if (!walletClient.account) {
415
+ throw new SolanaBridgeError(
416
+ "NO_WALLET_CLIENT",
417
+ "WalletClient must have an account attached.",
418
+ );
419
+ }
420
+
421
+ const account = walletClient.account;
422
+ const recipient = options.evmRecipient ?? account.address;
423
+
424
+ // Poll Circle IRIS for attestation
425
+ const attestation = await pollForAttestation(
426
+ burnParams.messageHash,
427
+ burnParams.sourceDomain,
428
+ attestationApiUrl,
429
+ );
430
+
431
+ // Submit receiveMessage on EVM
432
+ const transmitterAddress = MESSAGE_TRANSMITTER_V2_MAP[toChain];
433
+ const destChain = VIEM_CHAINS[toChain];
434
+ const destPublicClient = createPublicClient({
435
+ chain: destChain,
436
+ transport: http(evmRpcUrl),
437
+ }) as PublicClient;
438
+
439
+ const transmitter = getContract({
440
+ address: transmitterAddress,
441
+ abi: MessageTransmitterV2Abi,
442
+ client: { public: destPublicClient, wallet: walletClient },
443
+ });
444
+
445
+ let mintTxHash: Hash;
446
+ try {
447
+ mintTxHash = await transmitter.write.receiveMessage(
448
+ [burnParams.messageBytes, attestation],
449
+ { account, chain: destChain },
450
+ );
451
+ } catch (err: unknown) {
452
+ const msg = err instanceof Error ? err.message : String(err);
453
+ throw new SolanaBridgeError(
454
+ "MINT_FAILED",
455
+ `CCTP receiveMessage on ${toChain} failed: ${msg}.`,
456
+ );
457
+ }
458
+
459
+ const mintReceipt = await destPublicClient.waitForTransactionReceipt({
460
+ hash: mintTxHash,
461
+ });
462
+ if (mintReceipt.status !== "success") {
463
+ throw new SolanaBridgeError(
464
+ "MINT_FAILED",
465
+ `receiveMessage reverted on ${toChain} (tx: ${mintTxHash}).`,
466
+ );
467
+ }
468
+
469
+ // Parse amount from mint receipt logs (MintAndWithdraw event)
470
+ // Fallback: return 0n if event not found (amount can be retrieved from the original Solana tx)
471
+ const amount = parseMintAmount(mintReceipt.logs);
472
+
473
+ return {
474
+ mintTxHash,
475
+ amount,
476
+ toChain,
477
+ recipient,
478
+ elapsedMs: Date.now() - startMs,
479
+ };
480
+ }
481
+
482
+ // ─── Shared helpers ───
483
+
484
+ /**
485
+ * Extract CCTP MessageSent event from EVM transaction logs.
486
+ * Event topic: keccak256("MessageSent(bytes)") = 0x8c5261668696ce22758910d05bab8f186d6eb247ceac2af2e82c7dc17669b036
487
+ */
488
+ function extractMessageSent(
489
+ logs: readonly { topics: readonly Hex[]; data: Hex }[],
490
+ ): { messageBytes: Hex; messageHash: Hex } {
491
+ const MESSAGE_SENT_TOPIC =
492
+ "0x8c5261668696ce22758910d05bab8f186d6eb247ceac2af2e82c7dc17669b036";
493
+ for (const log of logs) {
494
+ if (log.topics[0]?.toLowerCase() === MESSAGE_SENT_TOPIC.toLowerCase()) {
495
+ const dataHex = log.data.slice(2);
496
+ if (dataHex.length < 128) continue;
497
+ const lengthHex = dataHex.slice(64, 128);
498
+ const messageLength = parseInt(lengthHex, 16);
499
+ if (messageLength === 0) continue;
500
+ const messageBytesHex = dataHex.slice(128, 128 + messageLength * 2);
501
+ const messageBytes = `0x${messageBytesHex}` as Hex;
502
+ const messageHash = keccak256(messageBytes);
503
+ return { messageBytes, messageHash };
504
+ }
505
+ }
506
+ throw new SolanaBridgeError(
507
+ "BURN_FAILED",
508
+ "Could not find MessageSent event in burn transaction receipt.",
509
+ );
510
+ }
511
+
512
+ /** Parse USDC amount from MintAndWithdraw event logs (best-effort). */
513
+ function parseMintAmount(
514
+ logs: readonly { topics: readonly Hex[]; data: Hex }[],
515
+ ): bigint {
516
+ // MintAndWithdraw(address,uint256,address) — topic0 = keccak256("MintAndWithdraw(address,uint256,address)")
517
+ const MINT_AND_WITHDRAW_TOPIC =
518
+ "0x1b2a7ff080b8cb6ff436ce0372e399692bbfb6d4ae5766fd8d58a7b8cc6142e9";
519
+ for (const log of logs) {
520
+ if (
521
+ log.topics[0]?.toLowerCase() === MINT_AND_WITHDRAW_TOPIC.toLowerCase()
522
+ ) {
523
+ // amount is the second indexed param OR first data word
524
+ if (log.data.length >= 66) {
525
+ return BigInt(`0x${log.data.slice(2, 66)}`);
526
+ }
527
+ }
528
+ }
529
+ return 0n; // fallback: caller can determine amount from source transaction
530
+ }
531
+
532
+ /** Poll Circle IRIS attestation API. */
533
+ async function pollForAttestation(
534
+ messageHash: Hex,
535
+ sourceDomain: number,
536
+ apiUrl: string,
537
+ ): Promise<Hex> {
538
+ const url = `${apiUrl}/v2/messages/${sourceDomain}/${messageHash}`;
539
+ for (let attempt = 0; attempt < MAX_ATTESTATION_POLLS; attempt++) {
540
+ let response: { status: string; attestation?: Hex | null; error?: string };
541
+ try {
542
+ // @duplicate-component-audit-allow Circle attestation polling is not an LLM generation call.
543
+ const res = await fetch(url, { headers: { Accept: "application/json" } });
544
+ if (!res.ok) {
545
+ if (res.status === 404) {
546
+ await sleep(ATTESTATION_POLL_INTERVAL_MS);
547
+ continue;
548
+ }
549
+ const body = await res.text().catch(() => "");
550
+ throw new SolanaBridgeError(
551
+ "ATTESTATION_ERROR",
552
+ `Circle API returned HTTP ${res.status}: ${body}.`,
553
+ );
554
+ }
555
+ response = (await res.json()) as typeof response;
556
+ } catch (err: unknown) {
557
+ if (err instanceof SolanaBridgeError) throw err;
558
+ const msg = err instanceof Error ? err.message : String(err);
559
+ throw new SolanaBridgeError(
560
+ "ATTESTATION_ERROR",
561
+ `Failed to reach Circle IRIS API: ${msg}.`,
562
+ );
563
+ }
564
+
565
+ if (response.status === "complete" && response.attestation)
566
+ return response.attestation;
567
+ if (response.status === "error") {
568
+ throw new SolanaBridgeError(
569
+ "ATTESTATION_ERROR",
570
+ `Circle attestation failed: ${response.error ?? "unknown error"}.`,
571
+ );
572
+ }
573
+ await sleep(ATTESTATION_POLL_INTERVAL_MS);
574
+ }
575
+ throw new SolanaBridgeError(
576
+ "ATTESTATION_TIMEOUT",
577
+ `Attestation not received after ${MAX_ATTESTATION_POLLS} attempts. Message hash: ${messageHash}.`,
578
+ );
579
+ }
580
+
581
+ function sleep(ms: number): Promise<void> {
582
+ return new Promise((resolve) => setTimeout(resolve, ms));
583
+ }
584
+
585
+ // ─── Error Class ───
586
+
587
+ export type SolanaBridgeErrorCode =
588
+ | "NO_WALLET_CLIENT"
589
+ | "INVALID_AMOUNT"
590
+ | "INVALID_RECIPIENT"
591
+ | "INSUFFICIENT_ALLOWANCE"
592
+ | "BURN_FAILED"
593
+ | "ATTESTATION_ERROR"
594
+ | "ATTESTATION_TIMEOUT"
595
+ | "MINT_FAILED";
596
+
597
+ export class SolanaBridgeError extends Error {
598
+ readonly code: SolanaBridgeErrorCode;
599
+ constructor(code: SolanaBridgeErrorCode, message: string) {
600
+ super(`[SolanaBridge:${code}] ${message}`);
601
+ this.code = code;
602
+ this.name = "SolanaBridgeError";
603
+ }
604
+ }