integration.test.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. import {
  2. broadcastTransaction,
  3. Cl,
  4. fetchCallReadOnlyFunction,
  5. fetchContractMapEntry,
  6. fetchNonce,
  7. makeContractCall,
  8. makeContractDeploy,
  9. Pc,
  10. privateKeyToAddress,
  11. TupleCV,
  12. UIntCV,
  13. } from "@stacks/transactions";
  14. import fs from "fs";
  15. import path from "path";
  16. import { describe, expect, it } from "vitest";
  17. import { STACKS_API_URL, STACKS_PRIVATE_KEY } from "./lib/constants";
  18. import {
  19. expectNoStacksVAA,
  20. expectVAA,
  21. waitForTransactionSuccess,
  22. wormhole,
  23. } from "./lib/helpers";
  24. const root = path.resolve(process.cwd(), "../");
  25. describe("Stacks Wormhole Integration Tests", () => {
  26. it("should deploy stacks contracts", async () => {
  27. const ADDRESS = privateKeyToAddress(STACKS_PRIVATE_KEY, "devnet");
  28. const contractPath = path.resolve(root, "contracts");
  29. const dependencyPath = path.resolve(root, "contracts/dependencies");
  30. const rewriteClarity = (code: string) => {
  31. return code
  32. .replaceAll("SP2J933XB2CP2JQ1A4FGN8JA968BBG3NK3EKZ7Q9F", ADDRESS)
  33. .replaceAll("SP1E0XBN9T4B10E9QMR7XMFJPMA19D77WY3KP2QKC", ADDRESS)
  34. .replaceAll("SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM", ADDRESS);
  35. };
  36. const dependencyFiles = [
  37. "trait-sip-010.clar",
  38. "proposal-trait.clar",
  39. "extension-trait.clar",
  40. "executor-dao.clar",
  41. "trait-semi-fungible.clar",
  42. "token-amm-pool-v2-01.clar",
  43. "liquidity-locker.clar",
  44. "clarity-stacks.clar",
  45. "trait-flash-loan-user.clar",
  46. "amm-vault-v2-01.clar",
  47. "amm-registry-v2-01.clar",
  48. "amm-pool-v2-01.clar",
  49. "code-body-prover.clar",
  50. "clarity-stacks-helper.clar",
  51. "self-listing-helper-v3.clar",
  52. "hk-ecc-v1.clar",
  53. "hk-cursor-v2.clar",
  54. "hk-merkle-tree-keccak160-v1.clar",
  55. ].map((file) => path.join(dependencyPath, file));
  56. const contractFiles = [
  57. "wormhole-core-state.clar",
  58. "wormhole-trait-core-v2.clar",
  59. "wormhole-core-proxy-v2.clar",
  60. "wormhole-trait-export-v1.clar",
  61. "wormhole-trait-governance-v1.clar",
  62. "wormhole-core-v4.clar",
  63. ].map((filename) => path.join(contractPath, filename));
  64. const versionMap = {
  65. "executor-dao.clar": 3,
  66. } as Record<string, number>;
  67. const contracts = [...dependencyFiles, ...contractFiles].map(
  68. (filePath) => ({
  69. name: path.basename(filePath).replace(".clar", ""),
  70. filename: path.basename(filePath),
  71. code: rewriteClarity(fs.readFileSync(filePath, "utf8")),
  72. })
  73. );
  74. let nonce = await fetchNonce({
  75. address: ADDRESS,
  76. client: { baseUrl: STACKS_API_URL },
  77. });
  78. const results = {
  79. totalContracts: contracts.length,
  80. successfulDeployments: 0,
  81. contracts: [] as string[],
  82. deployedTxIds: [] as string[],
  83. startingNonce: nonce,
  84. };
  85. console.log(
  86. `Deploying ${contracts.length} contracts starting with nonce ${nonce}`
  87. );
  88. for (const contract of contracts) {
  89. const transaction = await makeContractDeploy({
  90. contractName: contract.name,
  91. codeBody: contract.code,
  92. clarityVersion: versionMap?.[contract.filename] ?? 3,
  93. senderKey: STACKS_PRIVATE_KEY,
  94. nonce,
  95. network: "devnet",
  96. client: { baseUrl: STACKS_API_URL },
  97. });
  98. const response = await broadcastTransaction({
  99. transaction,
  100. network: "devnet",
  101. client: { baseUrl: STACKS_API_URL },
  102. });
  103. if (
  104. "error" in response &&
  105. response.reason === "ContractAlreadyExists"
  106. // Allow pre existing contracts only for local testing
  107. ) {
  108. console.log(
  109. `Contract ${contract.name} already exists, skipping deployment`
  110. );
  111. results.successfulDeployments++;
  112. results.contracts.push(contract.name);
  113. continue;
  114. } else if ("error" in response) {
  115. throw new Error(
  116. `Deploy failed for ${contract.name}: ${response.error} ${response.reason}`
  117. );
  118. }
  119. expect(response.txid).toBeDefined();
  120. expect(response.txid.length).toBe(64);
  121. console.log(`Deployed ${contract.name}: ${response.txid}`);
  122. // Wait for transaction to be successful before proceeding
  123. await waitForTransactionSuccess(response.txid);
  124. results.successfulDeployments++;
  125. results.contracts.push(contract.name);
  126. results.deployedTxIds.push(response.txid);
  127. nonce += 1n;
  128. }
  129. expect(results.totalContracts).toBeGreaterThan(0);
  130. expect(results.successfulDeployments).toBe(results.totalContracts);
  131. expect(results.contracts).toContain("wormhole-core-state");
  132. expect(results.contracts).toContain("wormhole-core-v4");
  133. expect(results.contracts).toContain("wormhole-core-proxy-v2");
  134. });
  135. it("should initialize stacks core contract", async () => {
  136. const ADDRESS = privateKeyToAddress(STACKS_PRIVATE_KEY, "devnet");
  137. const nonce = await fetchNonce({
  138. address: ADDRESS,
  139. client: { baseUrl: STACKS_API_URL },
  140. });
  141. const transaction = await makeContractCall({
  142. contractAddress: ADDRESS,
  143. contractName: "wormhole-core-v4",
  144. functionName: "initialize",
  145. functionArgs: [Cl.none()],
  146. senderKey: STACKS_PRIVATE_KEY,
  147. fee: 50_000,
  148. nonce,
  149. network: "devnet",
  150. client: { baseUrl: STACKS_API_URL },
  151. });
  152. const response = await broadcastTransaction({
  153. transaction,
  154. network: "devnet",
  155. client: { baseUrl: STACKS_API_URL },
  156. });
  157. if ("txid" in response && response.txid) {
  158. console.log(`Initialized core: ${response.txid}`);
  159. try {
  160. await waitForTransactionSuccess(response.txid);
  161. } catch (error) {
  162. // Check if it's the "already initialized" error (u10003)
  163. if (
  164. // Allow already initialized only for local testing
  165. error instanceof Error &&
  166. error.message.includes("(err u10003)")
  167. ) {
  168. console.log(`Core already initialized, continuing...`);
  169. } else throw error;
  170. }
  171. const owner = await fetchCallReadOnlyFunction({
  172. contractAddress: ADDRESS,
  173. contractName: "wormhole-core-state",
  174. functionName: "get-active-wormhole-core-contract",
  175. functionArgs: [],
  176. network: "devnet",
  177. client: { baseUrl: STACKS_API_URL },
  178. senderAddress: ADDRESS,
  179. });
  180. expect(owner).toBeDefined();
  181. console.log(`Core initialization verified`);
  182. } else {
  183. console.error(`Failed to initialize core:`, response);
  184. throw new Error(`Core initialization failed`);
  185. }
  186. });
  187. it("should upgrade guardian set", async () => {
  188. const ADDRESS = privateKeyToAddress(STACKS_PRIVATE_KEY, "devnet");
  189. const exportedVars = (await fetchCallReadOnlyFunction({
  190. contractAddress: ADDRESS,
  191. contractName: "wormhole-core-v4",
  192. functionName: "get-exported-vars",
  193. functionArgs: [],
  194. network: "devnet",
  195. client: { baseUrl: STACKS_API_URL },
  196. senderAddress: ADDRESS,
  197. })) as TupleCV<{ "active-guardian-set-id": UIntCV }>;
  198. const activeGuardianSetId = Number(
  199. exportedVars.value["active-guardian-set-id"].value
  200. );
  201. const keychain = wormhole.generateGuardianSetKeychain(19);
  202. const guardianSetUpgrade = wormhole.generateGuardianSetUpdateVaa(
  203. keychain,
  204. activeGuardianSetId + 1
  205. );
  206. const nonce = await fetchNonce({
  207. address: ADDRESS,
  208. client: { baseUrl: STACKS_API_URL },
  209. });
  210. const transaction = await makeContractCall({
  211. contractAddress: ADDRESS,
  212. contractName: "wormhole-core-v4",
  213. functionName: "guardian-set-upgrade",
  214. functionArgs: [
  215. Cl.buffer(guardianSetUpgrade.vaa),
  216. Cl.list(guardianSetUpgrade.uncompressedPublicKeys),
  217. ],
  218. senderKey: STACKS_PRIVATE_KEY,
  219. fee: 100_000,
  220. nonce,
  221. network: "devnet",
  222. client: { baseUrl: STACKS_API_URL },
  223. });
  224. const response = await broadcastTransaction({
  225. transaction,
  226. network: "devnet",
  227. client: { baseUrl: STACKS_API_URL },
  228. });
  229. if ("txid" in response && response.txid) {
  230. console.log(`Guardian set upgrade: ${response.txid}`);
  231. try {
  232. await waitForTransactionSuccess(response.txid);
  233. const guardianSet = await fetchContractMapEntry({
  234. contractAddress: ADDRESS,
  235. contractName: "wormhole-core-state",
  236. mapName: "guardian-sets",
  237. mapKey: Cl.uint(1),
  238. network: "devnet",
  239. client: { baseUrl: STACKS_API_URL },
  240. });
  241. expect(guardianSet).toBeDefined();
  242. console.log(`Guardian set upgrade verified`);
  243. } catch (error) {
  244. if (
  245. // Allow existing guardian set only for local testing
  246. error instanceof Error &&
  247. error.message.includes("(err u1102)")
  248. ) {
  249. console.log(`Guardian set upgrade failed, continuing...`);
  250. } else {
  251. throw error;
  252. }
  253. }
  254. } else {
  255. console.error(`Failed to upgrade guardian set:`, response);
  256. throw new Error(`Guardian set upgrade failed`);
  257. }
  258. });
  259. it("should post and spy onmessage", async () => {
  260. const ADDRESS = privateKeyToAddress(STACKS_PRIVATE_KEY, "devnet");
  261. const payload = Buffer.from("test-payload-success-case");
  262. const messageNonce = Math.floor(Math.random() * 0xffffffff);
  263. const spyPromise = expectVAA(payload);
  264. const nonce = await fetchNonce({
  265. address: ADDRESS,
  266. client: { baseUrl: STACKS_API_URL },
  267. });
  268. const transaction = await makeContractCall({
  269. contractAddress: ADDRESS,
  270. contractName: "wormhole-core-v4",
  271. functionName: "post-message",
  272. functionArgs: [Cl.buffer(payload), Cl.uint(messageNonce), Cl.none()],
  273. postConditionMode: "allow",
  274. senderKey: STACKS_PRIVATE_KEY,
  275. fee: 100_000,
  276. nonce,
  277. network: "devnet",
  278. client: { baseUrl: STACKS_API_URL },
  279. });
  280. const response = await broadcastTransaction({
  281. transaction,
  282. network: "devnet",
  283. client: { baseUrl: STACKS_API_URL },
  284. });
  285. if ("error" in response) throw new Error(response.error);
  286. console.log(`Posted message: ${response.txid}`);
  287. await waitForTransactionSuccess(response.txid);
  288. expect(response.txid).toBeDefined();
  289. expect(response.txid.length).toBe(64);
  290. await expect(spyPromise).resolves.toBeUndefined();
  291. });
  292. it("should spy but not find VAA for faulty transaction (abort_by_post_condition)", async () => {
  293. const ADDRESS = privateKeyToAddress(STACKS_PRIVATE_KEY, "devnet");
  294. const payload = Buffer.from("test-payload-abort-by-post-condition");
  295. const messageNonce = Math.floor(Math.random() * 0xffffffff);
  296. const spyPromise = expectNoStacksVAA();
  297. const nonce = await fetchNonce({
  298. address: ADDRESS,
  299. client: { baseUrl: STACKS_API_URL },
  300. });
  301. const transaction = await makeContractCall({
  302. contractAddress: ADDRESS,
  303. contractName: "wormhole-core-v4",
  304. functionName: "post-message",
  305. functionArgs: [Cl.buffer(payload), Cl.uint(messageNonce), Cl.none()],
  306. postConditionMode: "allow",
  307. postConditions: [Pc.origin().willSendEq(66).ustx()],
  308. senderKey: STACKS_PRIVATE_KEY,
  309. fee: 100_000,
  310. nonce,
  311. network: "devnet",
  312. client: { baseUrl: STACKS_API_URL },
  313. });
  314. const response = await broadcastTransaction({
  315. transaction,
  316. network: "devnet",
  317. client: { baseUrl: STACKS_API_URL },
  318. });
  319. if ("error" in response) throw new Error(response.error);
  320. console.log(
  321. `Posted faulty message (abort_by_post_condition): ${response.txid}`
  322. );
  323. await waitForTransactionSuccess(response.txid);
  324. expect(response.txid).toBeDefined();
  325. expect(response.txid.length).toBe(64);
  326. await expect(spyPromise).resolves.toBeUndefined();
  327. });
  328. it("should spy but not find VAA for faulty transaction (abort_by_response) #1", async () => {
  329. const ADDRESS = privateKeyToAddress(STACKS_PRIVATE_KEY, "devnet");
  330. const payload = Buffer.from("test-payload-abort-by-response-1");
  331. const spyPromise = expectNoStacksVAA();
  332. const nonce = await fetchNonce({
  333. address: ADDRESS,
  334. client: { baseUrl: STACKS_API_URL },
  335. });
  336. // Deploy a contract that calls post-message on deploy
  337. // The contract-call succeeds but transaction fails because response isn't handled
  338. const clarityCode = `(begin
  339. (contract-call? '${ADDRESS}.wormhole-core-v4 post-message
  340. 0x${payload.toString("hex")}
  341. u42
  342. none)
  343. (err u1))`;
  344. const transaction = await makeContractDeploy({
  345. contractName: `test-post-message-abort-${nonce}`,
  346. codeBody: clarityCode,
  347. clarityVersion: 3,
  348. senderKey: STACKS_PRIVATE_KEY,
  349. fee: 100_000,
  350. nonce,
  351. network: "devnet",
  352. client: { baseUrl: STACKS_API_URL },
  353. });
  354. const response = await broadcastTransaction({
  355. transaction,
  356. network: "devnet",
  357. client: { baseUrl: STACKS_API_URL },
  358. });
  359. if ("error" in response) {
  360. console.log(`Deploy failed: ${JSON.stringify(response, null, 2)}`);
  361. throw new Error(response.error);
  362. }
  363. console.log(`Posted faulty message (abort_by_response): ${response.txid}`);
  364. try {
  365. await waitForTransactionSuccess(response.txid);
  366. } catch (error) {
  367. if (error instanceof Error && error.message.includes("(err none)")) {
  368. console.log(`Transaction failed as expected.`);
  369. }
  370. }
  371. expect(response.txid).toBeDefined();
  372. expect(response.txid.length).toBe(64);
  373. await expect(spyPromise).resolves.toBeUndefined();
  374. });
  375. it("should spy but not find VAA for faulty transaction (abort_by_response) #2", async () => {
  376. const ADDRESS = privateKeyToAddress(STACKS_PRIVATE_KEY, "devnet");
  377. const payload = Buffer.from("test-payload-abort-by-response-2");
  378. const spyPromise = expectNoStacksVAA();
  379. let nonce = await fetchNonce({
  380. address: ADDRESS,
  381. client: { baseUrl: STACKS_API_URL },
  382. });
  383. // STEP 1: Deploy a contract with a public function that calls post-message and returns error
  384. const clarityCode = `(define-public (post-and-fail)
  385. (begin
  386. (try! (contract-call? '${ADDRESS}.wormhole-core-v4 post-message
  387. 0x${payload.toString("hex")}
  388. u43
  389. none))
  390. (err u1)))`;
  391. const deployTransaction = await makeContractDeploy({
  392. contractName: `test-post-message-two-step-${nonce}`,
  393. codeBody: clarityCode,
  394. clarityVersion: 3,
  395. senderKey: STACKS_PRIVATE_KEY,
  396. fee: 100_000,
  397. nonce,
  398. network: "devnet",
  399. client: { baseUrl: STACKS_API_URL },
  400. });
  401. const deployResponse = await broadcastTransaction({
  402. transaction: deployTransaction,
  403. network: "devnet",
  404. client: { baseUrl: STACKS_API_URL },
  405. });
  406. if ("error" in deployResponse) {
  407. console.log(`Deploy failed: ${JSON.stringify(deployResponse, null, 2)}`);
  408. throw new Error(deployResponse.error);
  409. }
  410. console.log(`Deployed two-step test contract: ${deployResponse.txid}`);
  411. try {
  412. await waitForTransactionSuccess(deployResponse.txid);
  413. } catch (error) {
  414. if (error instanceof Error && error.message.includes("(err")) {
  415. console.log(`Transaction failed as expected.`);
  416. }
  417. }
  418. expect(deployResponse.txid).toBeDefined();
  419. expect(deployResponse.txid.length).toBe(64);
  420. nonce += 1n;
  421. // STEP 2: Call the public function that will post-message and return error
  422. const callTransaction = await makeContractCall({
  423. contractAddress: ADDRESS,
  424. contractName: `test-post-message-two-step-${nonce - 1n}`,
  425. functionName: "post-and-fail",
  426. functionArgs: [],
  427. postConditionMode: "allow",
  428. senderKey: STACKS_PRIVATE_KEY,
  429. fee: 100_000,
  430. nonce,
  431. network: "devnet",
  432. client: { baseUrl: STACKS_API_URL },
  433. });
  434. const callResponse = await broadcastTransaction({
  435. transaction: callTransaction,
  436. network: "devnet",
  437. client: { baseUrl: STACKS_API_URL },
  438. });
  439. if ("error" in callResponse) {
  440. console.log(`Call failed: ${JSON.stringify(callResponse, null, 2)}`);
  441. throw new Error(callResponse.error);
  442. }
  443. console.log(
  444. `Posted faulty message (abort_by_response_two_step): ${callResponse.txid}`
  445. );
  446. try {
  447. await waitForTransactionSuccess(callResponse.txid);
  448. } catch (error) {
  449. if (error instanceof Error && error.message.includes("(err")) {
  450. console.log(`Transaction failed as expected.`);
  451. }
  452. }
  453. expect(callResponse.txid).toBeDefined();
  454. expect(callResponse.txid.length).toBe(64);
  455. await expect(spyPromise).resolves.toBeUndefined();
  456. });
  457. });