From 9e108905dfa04099e9cc1720f9398f6ab2e6185c Mon Sep 17 00:00:00 2001 From: mverzilli Date: Thu, 9 Apr 2026 15:52:34 +0000 Subject: [PATCH 01/24] chore: upgrade to v4.2.0-nightly.20260409 with local gregojuice - Upgrade @aztec/* npm packages to 4.2.0-nightly.20260409 - Upgrade Nargo.toml git tags to match - Use portal: links for @gregojuice/* packages (local dev) - Add deploy-subscription-fpc.ts script for local SubscriptionFPC deployment - Updated local.json with deployed contract addresses and subscriptionFPC config Co-Authored-By: Claude Opus 4.6 (1M context) --- contracts/amm/Nargo.toml | 6 +- contracts/proof_of_password/Nargo.toml | 6 +- package.json | 26 +- scripts/deploy-subscription-fpc.ts | 73 ++++ yarn.lock | 562 ++++++++++++------------- 5 files changed, 372 insertions(+), 301 deletions(-) create mode 100644 scripts/deploy-subscription-fpc.ts diff --git a/contracts/amm/Nargo.toml b/contracts/amm/Nargo.toml index 01e8d46..a3dde64 100644 --- a/contracts/amm/Nargo.toml +++ b/contracts/amm/Nargo.toml @@ -4,6 +4,6 @@ authors = [""] type = "contract" [dependencies] -aztec = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-aztecnr-rc.2", directory = "noir-projects/aztec-nr/aztec" } -token = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-aztecnr-rc.2", directory = "noir-projects/noir-contracts/contracts/app/token_contract" } -uint_note = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-aztecnr-rc.2", directory = "noir-projects/aztec-nr/uint-note" } +aztec = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-nightly.20260409", directory = "noir-projects/aztec-nr/aztec" } +token = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-nightly.20260409", directory = "noir-projects/noir-contracts/contracts/app/token_contract" } +uint_note = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-nightly.20260409", directory = "noir-projects/aztec-nr/uint-note" } diff --git a/contracts/proof_of_password/Nargo.toml b/contracts/proof_of_password/Nargo.toml index 37d6ff2..828567e 100644 --- a/contracts/proof_of_password/Nargo.toml +++ b/contracts/proof_of_password/Nargo.toml @@ -4,7 +4,7 @@ type = "contract" authors = [""] [dependencies] -aztec = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-aztecnr-rc.2", directory = "noir-projects/aztec-nr/aztec" } -token = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-aztecnr-rc.2", directory = "noir-projects/noir-contracts/contracts/app/token_contract" } +aztec = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-nightly.20260409", directory = "noir-projects/aztec-nr/aztec" } +token = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-nightly.20260409", directory = "noir-projects/noir-contracts/contracts/app/token_contract" } poseidon = { tag = "v0.1.1", git = "https://github.com/noir-lang/poseidon" } -compressed_string = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-aztecnr-rc.2", directory = "noir-projects/aztec-nr/compressed-string" } \ No newline at end of file +compressed_string = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-nightly.20260409", directory = "noir-projects/aztec-nr/compressed-string" } \ No newline at end of file diff --git a/package.json b/package.json index 8988b20..126afe7 100644 --- a/package.json +++ b/package.json @@ -27,20 +27,20 @@ "local-aztec:status": "node scripts/toggle-local-aztec.js status" }, "dependencies": { - "@aztec/accounts": "v4.2.0-aztecnr-rc.2", - "@aztec/aztec.js": "v4.2.0-aztecnr-rc.2", - "@aztec/constants": "v4.2.0-aztecnr-rc.2", - "@aztec/entrypoints": "v4.2.0-aztecnr-rc.2", - "@aztec/foundation": "v4.2.0-aztecnr-rc.2", - "@aztec/noir-contracts.js": "v4.2.0-aztecnr-rc.2", - "@aztec/protocol-contracts": "v4.2.0-aztecnr-rc.2", - "@aztec/pxe": "v4.2.0-aztecnr-rc.2", - "@aztec/stdlib": "v4.2.0-aztecnr-rc.2", - "@aztec/wallet-sdk": "v4.2.0-aztecnr-rc.2", + "@aztec/accounts": "4.2.0-nightly.20260409", + "@aztec/aztec.js": "4.2.0-nightly.20260409", + "@aztec/constants": "4.2.0-nightly.20260409", + "@aztec/entrypoints": "4.2.0-nightly.20260409", + "@aztec/foundation": "4.2.0-nightly.20260409", + "@aztec/noir-contracts.js": "4.2.0-nightly.20260409", + "@aztec/protocol-contracts": "4.2.0-nightly.20260409", + "@aztec/pxe": "4.2.0-nightly.20260409", + "@aztec/stdlib": "4.2.0-nightly.20260409", + "@aztec/wallet-sdk": "4.2.0-nightly.20260409", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", - "@gregojuice/contracts": "^0.0.10", - "@gregojuice/embedded-wallet": "^0.0.10", + "@gregojuice/contracts": "portal:/mnt/user-data/martin/gregojuice/packages/contracts", + "@gregojuice/embedded-wallet": "portal:/mnt/user-data/martin/gregojuice/packages/embedded-wallet", "@mui/icons-material": "^6.3.1", "@mui/material": "^6.3.1", "@mui/styles": "^6.3.1", @@ -51,7 +51,7 @@ "zod": "^3.23.8" }, "devDependencies": { - "@aztec/wallets": "v4.2.0-aztecnr-rc.2", + "@aztec/wallets": "4.2.0-nightly.20260409", "@eslint/js": "^9.18.0", "@playwright/test": "1.49.0", "@types/buffer-json": "^2", diff --git a/scripts/deploy-subscription-fpc.ts b/scripts/deploy-subscription-fpc.ts new file mode 100644 index 0000000..2962258 --- /dev/null +++ b/scripts/deploy-subscription-fpc.ts @@ -0,0 +1,73 @@ +/** + * Deploys the SubscriptionFPC contract to the local sandbox and updates local.json config. + * + * Uses the artifact from @gregojuice/contracts and deploys via gregoswap's own SDK + * to avoid version mismatch issues. + * + * Usage: node --experimental-transform-types scripts/deploy-subscription-fpc.ts + */ + +import fs from 'fs'; +import path from 'path'; +import { SubscriptionFPCContractArtifact } from '@gregojuice/contracts/artifacts/SubscriptionFPC'; +import { FunctionSelector } from '@aztec/stdlib/abi'; +import { Fr } from '@aztec/foundation/curves/bn254'; +import { Contract } from '@aztec/aztec.js/contracts'; +import { ProofOfPasswordContractArtifact } from '../contracts/target/ProofOfPassword.ts'; +import { AMMContractArtifact } from '../contracts/target/AMM.ts'; +import { setupWallet, getOrCreateDeployer } from './utils.ts'; + +async function main() { + const { wallet, paymentMethod } = await setupWallet('http://localhost:8080', 'local'); + const deployer = await getOrCreateDeployer(wallet, paymentMethod); + + console.log('Deploying SubscriptionFPC...'); + + // Generate a secret key for the FPC instance + const secretKey = Fr.random(); + const salt = Fr.random(); + + // Deploy using gregoswap's own SDK Contract.deploy + const { contract } = await Contract.deploy(wallet, SubscriptionFPCContractArtifact, [deployer]).send({ + from: deployer, + fee: { paymentMethod }, + contractAddressSalt: salt, + wait: { timeout: 120 }, + }); + + const fpcAddress = contract.address.toString(); + console.log('SubscriptionFPC deployed at:', fpcAddress); + console.log('Secret key:', secretKey.toString()); + + // Compute function selectors + const popFn = ProofOfPasswordContractArtifact.functions.find(f => f.name === 'check_password_and_mint'); + const popSelector = await FunctionSelector.fromNameAndParameters(popFn!.name, popFn!.parameters); + + const ammFn = AMMContractArtifact.functions.find(f => f.name === 'swap_tokens_for_exact_tokens_from'); + const ammSelector = await FunctionSelector.fromNameAndParameters(ammFn!.name, ammFn!.parameters); + + // Update local.json + const configPath = path.join(import.meta.dirname, '../src/config/networks/local.json'); + const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); + + config.subscriptionFPC = { + address: fpcAddress, + secretKey: secretKey.toString(), + functions: { + [config.contracts.pop]: { + [popSelector.toString()]: 0, + }, + [config.contracts.amm]: { + [ammSelector.toString()]: 0, + }, + }, + }; + + fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); + console.log(`\nUpdated ${configPath} with subscriptionFPC config.`); +} + +main().catch(err => { + console.error(err); + process.exit(1); +}); diff --git a/yarn.lock b/yarn.lock index cea859f..d0decab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -645,66 +645,66 @@ __metadata: languageName: node linkType: hard -"@aztec/accounts@npm:4.2.0-aztecnr-rc.2, @aztec/accounts@npm:v4.2.0-aztecnr-rc.2": - version: 4.2.0-aztecnr-rc.2 - resolution: "@aztec/accounts@npm:4.2.0-aztecnr-rc.2" - dependencies: - "@aztec/aztec.js": "npm:4.2.0-aztecnr-rc.2" - "@aztec/entrypoints": "npm:4.2.0-aztecnr-rc.2" - "@aztec/ethereum": "npm:4.2.0-aztecnr-rc.2" - "@aztec/foundation": "npm:4.2.0-aztecnr-rc.2" - "@aztec/stdlib": "npm:4.2.0-aztecnr-rc.2" +"@aztec/accounts@npm:4.2.0-nightly.20260409": + version: 4.2.0-nightly.20260409 + resolution: "@aztec/accounts@npm:4.2.0-nightly.20260409" + dependencies: + "@aztec/aztec.js": "npm:4.2.0-nightly.20260409" + "@aztec/entrypoints": "npm:4.2.0-nightly.20260409" + "@aztec/ethereum": "npm:4.2.0-nightly.20260409" + "@aztec/foundation": "npm:4.2.0-nightly.20260409" + "@aztec/stdlib": "npm:4.2.0-nightly.20260409" tslib: "npm:^2.4.0" - checksum: 10c0/746711e7194a8c215eab294b3d45c548878bde4c980d73b67e571f34932aba0cf9dc050ea5c48850d8689395191627c3f1a293135833bc1733eac1ef222e2929 + checksum: 10c0/f3b7abde1f11a5e869d06d3cf3c5686b92eea2b7aaae64f5790f77219d38920f5d3c97b018c7bab28916c87edb34208b2907af76d856faff8c7019189236885c languageName: node linkType: hard -"@aztec/aztec.js@npm:4.2.0-aztecnr-rc.2, @aztec/aztec.js@npm:v4.2.0-aztecnr-rc.2": - version: 4.2.0-aztecnr-rc.2 - resolution: "@aztec/aztec.js@npm:4.2.0-aztecnr-rc.2" +"@aztec/aztec.js@npm:4.2.0-nightly.20260409": + version: 4.2.0-nightly.20260409 + resolution: "@aztec/aztec.js@npm:4.2.0-nightly.20260409" dependencies: - "@aztec/constants": "npm:4.2.0-aztecnr-rc.2" - "@aztec/entrypoints": "npm:4.2.0-aztecnr-rc.2" - "@aztec/ethereum": "npm:4.2.0-aztecnr-rc.2" - "@aztec/foundation": "npm:4.2.0-aztecnr-rc.2" - "@aztec/l1-artifacts": "npm:4.2.0-aztecnr-rc.2" - "@aztec/protocol-contracts": "npm:4.2.0-aztecnr-rc.2" - "@aztec/stdlib": "npm:4.2.0-aztecnr-rc.2" + "@aztec/constants": "npm:4.2.0-nightly.20260409" + "@aztec/entrypoints": "npm:4.2.0-nightly.20260409" + "@aztec/ethereum": "npm:4.2.0-nightly.20260409" + "@aztec/foundation": "npm:4.2.0-nightly.20260409" + "@aztec/l1-artifacts": "npm:4.2.0-nightly.20260409" + "@aztec/protocol-contracts": "npm:4.2.0-nightly.20260409" + "@aztec/stdlib": "npm:4.2.0-nightly.20260409" axios: "npm:^1.13.5" tslib: "npm:^2.4.0" viem: "npm:@aztec/viem@2.38.2" zod: "npm:^3.23.8" - checksum: 10c0/7d513436c427c45c399dcc843dba36211d26d1655a8679725edb64b5df9dcf7de48d9c537970c3e67086dfeecd3624befecdff4b8f8052a5958b359736bac31a + checksum: 10c0/e45b348e7f71979e2fd8dd33991b28c5f127239c2b7571d5616e6ef34d7c49412e5bfd2b460ae154f9e2664fa4e61838ad94d0c263b852b4f899ddca0e40f945 languageName: node linkType: hard -"@aztec/bb-prover@npm:4.2.0-aztecnr-rc.2": - version: 4.2.0-aztecnr-rc.2 - resolution: "@aztec/bb-prover@npm:4.2.0-aztecnr-rc.2" +"@aztec/bb-prover@npm:4.2.0-nightly.20260409": + version: 4.2.0-nightly.20260409 + resolution: "@aztec/bb-prover@npm:4.2.0-nightly.20260409" dependencies: - "@aztec/bb.js": "npm:4.2.0-aztecnr-rc.2" - "@aztec/constants": "npm:4.2.0-aztecnr-rc.2" - "@aztec/foundation": "npm:4.2.0-aztecnr-rc.2" - "@aztec/noir-noirc_abi": "npm:4.2.0-aztecnr-rc.2" - "@aztec/noir-protocol-circuits-types": "npm:4.2.0-aztecnr-rc.2" - "@aztec/noir-types": "npm:4.2.0-aztecnr-rc.2" - "@aztec/simulator": "npm:4.2.0-aztecnr-rc.2" - "@aztec/stdlib": "npm:4.2.0-aztecnr-rc.2" - "@aztec/telemetry-client": "npm:4.2.0-aztecnr-rc.2" - "@aztec/world-state": "npm:4.2.0-aztecnr-rc.2" + "@aztec/bb.js": "npm:4.2.0-nightly.20260409" + "@aztec/constants": "npm:4.2.0-nightly.20260409" + "@aztec/foundation": "npm:4.2.0-nightly.20260409" + "@aztec/noir-noirc_abi": "npm:4.2.0-nightly.20260409" + "@aztec/noir-protocol-circuits-types": "npm:4.2.0-nightly.20260409" + "@aztec/noir-types": "npm:4.2.0-nightly.20260409" + "@aztec/simulator": "npm:4.2.0-nightly.20260409" + "@aztec/stdlib": "npm:4.2.0-nightly.20260409" + "@aztec/telemetry-client": "npm:4.2.0-nightly.20260409" + "@aztec/world-state": "npm:4.2.0-nightly.20260409" commander: "npm:^12.1.0" pako: "npm:^2.1.0" source-map-support: "npm:^0.5.21" tslib: "npm:^2.4.0" bin: bb-cli: dest/bb/index.js - checksum: 10c0/ace75174c8a69d07aa92f4bc0656abeb6e506693f32738298d850e0507e038e2d0c303c3f9183edda26d9a25d2e4aaf26ac3c78de15ec70d9b4ef9f2ee5af58a + checksum: 10c0/a7c8add943a9c182095baba743cd17a98da9fe4dcde289ac9bb82534d181be31899b25873d1c905958873166431811af5226e879bde463f91e8f2d4e92b09c4b languageName: node linkType: hard -"@aztec/bb.js@npm:4.2.0-aztecnr-rc.2": - version: 4.2.0-aztecnr-rc.2 - resolution: "@aztec/bb.js@npm:4.2.0-aztecnr-rc.2" +"@aztec/bb.js@npm:4.2.0-nightly.20260409": + version: 4.2.0-nightly.20260409 + resolution: "@aztec/bb.js@npm:4.2.0-nightly.20260409" dependencies: comlink: "npm:^4.4.1" commander: "npm:^12.1.0" @@ -714,65 +714,65 @@ __metadata: tslib: "npm:^2.4.0" bin: bb: dest/node/bin/index.js - checksum: 10c0/9b471b0d6564841eebed8cfc47a2dd743362317ad83f3e4d6bb173afc8f71f02246f562e9db1b7bace3dbb23bcdb051c22019e463d83217f49a4a9c69104362c + checksum: 10c0/7a3b181de1343b212dac8ed5dfe799b1ca0798b13160e49e88d4be5ee05d1e6761a3b2130b25afd975307494589ceae36b5eab1c3ff92ba1a4367406d9401e3b languageName: node linkType: hard -"@aztec/blob-lib@npm:4.2.0-aztecnr-rc.2": - version: 4.2.0-aztecnr-rc.2 - resolution: "@aztec/blob-lib@npm:4.2.0-aztecnr-rc.2" +"@aztec/blob-lib@npm:4.2.0-nightly.20260409": + version: 4.2.0-nightly.20260409 + resolution: "@aztec/blob-lib@npm:4.2.0-nightly.20260409" dependencies: - "@aztec/constants": "npm:4.2.0-aztecnr-rc.2" - "@aztec/foundation": "npm:4.2.0-aztecnr-rc.2" + "@aztec/constants": "npm:4.2.0-nightly.20260409" + "@aztec/foundation": "npm:4.2.0-nightly.20260409" "@crate-crypto/node-eth-kzg": "npm:^0.10.0" tslib: "npm:^2.4.0" - checksum: 10c0/783add261c60a9dcaade735cd55544ac14afd846d2cc31769b91588447fb59506b3ec0c7872282d6b87a369f4cbc0a0f2ec3d8f6984d2d868738d1b284b388c3 + checksum: 10c0/f712c6d82c5895ca24d453911ee75fffa221cd6044ccea632b5396a2ac308640699dde77a34190b18190200ade3b5c0ffbf91e5cb9e0e1e8660fea4b97c0cebd languageName: node linkType: hard -"@aztec/builder@npm:4.2.0-aztecnr-rc.2": - version: 4.2.0-aztecnr-rc.2 - resolution: "@aztec/builder@npm:4.2.0-aztecnr-rc.2" +"@aztec/builder@npm:4.2.0-nightly.20260409": + version: 4.2.0-nightly.20260409 + resolution: "@aztec/builder@npm:4.2.0-nightly.20260409" dependencies: - "@aztec/foundation": "npm:4.2.0-aztecnr-rc.2" - "@aztec/stdlib": "npm:4.2.0-aztecnr-rc.2" + "@aztec/foundation": "npm:4.2.0-nightly.20260409" + "@aztec/stdlib": "npm:4.2.0-nightly.20260409" commander: "npm:^12.1.0" - checksum: 10c0/8f6e05da3f42af1cf0397f898b67cd12d6343dbed15537cd6c8e5953596d926e5b0d51cedff8c27837f9e633c409740c3cbd2b712b31ed3beb8a7e1453df5fdb + checksum: 10c0/44e067625daae1a377125801020535c033ae8d3fcfa7e6032f0588415a68a299814a87bba29dbc3ebe05e443843da2b2cba7c5ae1c538a4370c3b2937f8909d5 languageName: node linkType: hard -"@aztec/constants@npm:4.2.0-aztecnr-rc.2, @aztec/constants@npm:v4.2.0-aztecnr-rc.2": - version: 4.2.0-aztecnr-rc.2 - resolution: "@aztec/constants@npm:4.2.0-aztecnr-rc.2" +"@aztec/constants@npm:4.2.0-nightly.20260409": + version: 4.2.0-nightly.20260409 + resolution: "@aztec/constants@npm:4.2.0-nightly.20260409" dependencies: - "@aztec/foundation": "npm:4.2.0-aztecnr-rc.2" + "@aztec/foundation": "npm:4.2.0-nightly.20260409" tslib: "npm:^2.4.0" - checksum: 10c0/05a44435b45c49a0daa3459649dd361a69b3414cfa5ce476f6b7df818c707d0461ed4dfb133f185485c0670c4b66cbd994d0645cd197592b667524c6210dc32e + checksum: 10c0/aed35931ae458f098c1f68089a5a0d5db5c958ee8496e8d83f294ed3191bd5a0a2a3fca7da24fd819281071e0cab9854911ebdce247e2989f8c02fd9b46272ca languageName: node linkType: hard -"@aztec/entrypoints@npm:4.2.0-aztecnr-rc.2, @aztec/entrypoints@npm:v4.2.0-aztecnr-rc.2": - version: 4.2.0-aztecnr-rc.2 - resolution: "@aztec/entrypoints@npm:4.2.0-aztecnr-rc.2" +"@aztec/entrypoints@npm:4.2.0-nightly.20260409": + version: 4.2.0-nightly.20260409 + resolution: "@aztec/entrypoints@npm:4.2.0-nightly.20260409" dependencies: - "@aztec/constants": "npm:4.2.0-aztecnr-rc.2" - "@aztec/foundation": "npm:4.2.0-aztecnr-rc.2" - "@aztec/protocol-contracts": "npm:4.2.0-aztecnr-rc.2" - "@aztec/stdlib": "npm:4.2.0-aztecnr-rc.2" + "@aztec/constants": "npm:4.2.0-nightly.20260409" + "@aztec/foundation": "npm:4.2.0-nightly.20260409" + "@aztec/protocol-contracts": "npm:4.2.0-nightly.20260409" + "@aztec/stdlib": "npm:4.2.0-nightly.20260409" tslib: "npm:^2.4.0" zod: "npm:^3.23.8" - checksum: 10c0/ee2ca23db937c8047aa654b7d97edb1dc1cb0efe8bebcf902d95f4a1ae08f55997cef2375752a0ee974f1c5549e91e47b96db46517b491b0c1d21c4ea664d5e0 + checksum: 10c0/bce90b402990dcf4d1abd4656ac5f15ff43151fe54dfffcf165101cf1eee176ac935bc1ebb29b95d804f28bef0b82f6b0938eed957bb2578b928cb596079093d languageName: node linkType: hard -"@aztec/ethereum@npm:4.2.0-aztecnr-rc.2": - version: 4.2.0-aztecnr-rc.2 - resolution: "@aztec/ethereum@npm:4.2.0-aztecnr-rc.2" +"@aztec/ethereum@npm:4.2.0-nightly.20260409": + version: 4.2.0-nightly.20260409 + resolution: "@aztec/ethereum@npm:4.2.0-nightly.20260409" dependencies: - "@aztec/blob-lib": "npm:4.2.0-aztecnr-rc.2" - "@aztec/constants": "npm:4.2.0-aztecnr-rc.2" - "@aztec/foundation": "npm:4.2.0-aztecnr-rc.2" - "@aztec/l1-artifacts": "npm:4.2.0-aztecnr-rc.2" + "@aztec/blob-lib": "npm:4.2.0-nightly.20260409" + "@aztec/constants": "npm:4.2.0-nightly.20260409" + "@aztec/foundation": "npm:4.2.0-nightly.20260409" + "@aztec/l1-artifacts": "npm:4.2.0-nightly.20260409" "@viem/anvil": "npm:^0.0.10" dotenv: "npm:^16.0.3" lodash.chunk: "npm:^4.2.0" @@ -780,15 +780,15 @@ __metadata: tslib: "npm:^2.4.0" viem: "npm:@aztec/viem@2.38.2" zod: "npm:^3.23.8" - checksum: 10c0/bef1b0c5e5495816f144afdfece50b4b898be61a8128bb889776541fd4623b585256e9a399f97126ccbaed458adeac55d1770b82b79a2804887a97db6a0c6ad7 + checksum: 10c0/9d5e344877d62778b4150156b84626d3a241ba4d4cd66bb94aaed100c52f9ef458f54dee7af81547a80e597bfd7364a485d4590bf3e7be453dee89762657adbb languageName: node linkType: hard -"@aztec/foundation@npm:4.2.0-aztecnr-rc.2, @aztec/foundation@npm:v4.2.0-aztecnr-rc.2": - version: 4.2.0-aztecnr-rc.2 - resolution: "@aztec/foundation@npm:4.2.0-aztecnr-rc.2" +"@aztec/foundation@npm:4.2.0-nightly.20260409": + version: 4.2.0-nightly.20260409 + resolution: "@aztec/foundation@npm:4.2.0-nightly.20260409" dependencies: - "@aztec/bb.js": "npm:4.2.0-aztecnr-rc.2" + "@aztec/bb.js": "npm:4.2.0-nightly.20260409" "@koa/cors": "npm:^5.0.0" "@noble/curves": "npm:=1.7.0" "@noble/hashes": "npm:^1.6.1" @@ -810,169 +810,169 @@ __metadata: sha3: "npm:^2.1.4" undici: "npm:^5.28.5" zod: "npm:^3.23.8" - checksum: 10c0/28d44afd6f28c9f54f2d3567b46776344a09072be12d2b761dd81b128e5d6a092a77111b536aefe7a899e8f123b07074ee1be3f47b90e422e4502992f3746305 + checksum: 10c0/26878b2208b9ef11f9d4df4e077e1f4dafff044842295485e997c81d49b866a09e32b97f408b7fcb4154e0c3d488a499cc5f0713ecbe9c7c5c69de09ede7ff5d languageName: node linkType: hard -"@aztec/key-store@npm:4.2.0-aztecnr-rc.2": - version: 4.2.0-aztecnr-rc.2 - resolution: "@aztec/key-store@npm:4.2.0-aztecnr-rc.2" +"@aztec/key-store@npm:4.2.0-nightly.20260409": + version: 4.2.0-nightly.20260409 + resolution: "@aztec/key-store@npm:4.2.0-nightly.20260409" dependencies: - "@aztec/constants": "npm:4.2.0-aztecnr-rc.2" - "@aztec/foundation": "npm:4.2.0-aztecnr-rc.2" - "@aztec/kv-store": "npm:4.2.0-aztecnr-rc.2" - "@aztec/stdlib": "npm:4.2.0-aztecnr-rc.2" + "@aztec/constants": "npm:4.2.0-nightly.20260409" + "@aztec/foundation": "npm:4.2.0-nightly.20260409" + "@aztec/kv-store": "npm:4.2.0-nightly.20260409" + "@aztec/stdlib": "npm:4.2.0-nightly.20260409" tslib: "npm:^2.4.0" - checksum: 10c0/954fa6755b2dffe85d3e1f858cb89844782c240b0542d21fda5be0b8347cb1469b468409d7db23d6c0263022ca241727f96ef53d4e7e646d930417d71f2c0c11 + checksum: 10c0/f14bfe99146ce6dbebdb6f755d5ea4ac68585739437f43aec4351d0fcbc4af30de9a97062c71a088e3b6c62676600f3ca70c6f2d14d5254c95a3264371a32dfc languageName: node linkType: hard -"@aztec/kv-store@npm:4.2.0-aztecnr-rc.2": - version: 4.2.0-aztecnr-rc.2 - resolution: "@aztec/kv-store@npm:4.2.0-aztecnr-rc.2" +"@aztec/kv-store@npm:4.2.0-nightly.20260409": + version: 4.2.0-nightly.20260409 + resolution: "@aztec/kv-store@npm:4.2.0-nightly.20260409" dependencies: - "@aztec/constants": "npm:4.2.0-aztecnr-rc.2" - "@aztec/ethereum": "npm:4.2.0-aztecnr-rc.2" - "@aztec/foundation": "npm:4.2.0-aztecnr-rc.2" - "@aztec/native": "npm:4.2.0-aztecnr-rc.2" - "@aztec/stdlib": "npm:4.2.0-aztecnr-rc.2" + "@aztec/constants": "npm:4.2.0-nightly.20260409" + "@aztec/ethereum": "npm:4.2.0-nightly.20260409" + "@aztec/foundation": "npm:4.2.0-nightly.20260409" + "@aztec/native": "npm:4.2.0-nightly.20260409" + "@aztec/stdlib": "npm:4.2.0-nightly.20260409" idb: "npm:^8.0.0" lmdb: "npm:^3.2.0" msgpackr: "npm:^1.11.2" ohash: "npm:^2.0.11" ordered-binary: "npm:^1.5.3" - checksum: 10c0/d4403dd175d0a689ed4291cd128ce341cac389b1aa6cb83c481b6694d6955b3f1967e5474b9e54ae5830707855238f63ca0b3397f3cea534da6f6a0ecac410eb + checksum: 10c0/f9d60645a4c686e2091a330965d7c7296fa508dde1ce649dccf5d16e02dee57ce6181a253a7596c2a7e28c9f3fc2616096032dc3cd81cbeec98cc8bf0f6e99ac languageName: node linkType: hard -"@aztec/l1-artifacts@npm:4.2.0-aztecnr-rc.2": - version: 4.2.0-aztecnr-rc.2 - resolution: "@aztec/l1-artifacts@npm:4.2.0-aztecnr-rc.2" +"@aztec/l1-artifacts@npm:4.2.0-nightly.20260409": + version: 4.2.0-nightly.20260409 + resolution: "@aztec/l1-artifacts@npm:4.2.0-nightly.20260409" dependencies: tslib: "npm:^2.4.0" - checksum: 10c0/7ee413bf4cf147d301d93b1c25ec8b7c764ab901bcc72a11103d77a9c2af5eceaa3427c7d8c0b2185dc3e04adfdbc55ee92cb63ddba3811cc1c91c065118f528 + checksum: 10c0/70abe9e54be2f96d782d055be99716ec51593bae5d19300531bbc61d86e8823a8cf1d1356aed6b9634a0d553646353351bf7d220bc2d962b98b3b861f5fe4e90 languageName: node linkType: hard -"@aztec/merkle-tree@npm:4.2.0-aztecnr-rc.2": - version: 4.2.0-aztecnr-rc.2 - resolution: "@aztec/merkle-tree@npm:4.2.0-aztecnr-rc.2" +"@aztec/merkle-tree@npm:4.2.0-nightly.20260409": + version: 4.2.0-nightly.20260409 + resolution: "@aztec/merkle-tree@npm:4.2.0-nightly.20260409" dependencies: - "@aztec/foundation": "npm:4.2.0-aztecnr-rc.2" - "@aztec/kv-store": "npm:4.2.0-aztecnr-rc.2" - "@aztec/stdlib": "npm:4.2.0-aztecnr-rc.2" + "@aztec/foundation": "npm:4.2.0-nightly.20260409" + "@aztec/kv-store": "npm:4.2.0-nightly.20260409" + "@aztec/stdlib": "npm:4.2.0-nightly.20260409" sha256: "npm:^0.2.0" tslib: "npm:^2.4.0" - checksum: 10c0/f3aaf95af249a4c4b457609cd8085c420ee0e1a5491124031c18733c8e7389a3e0b78fdc6e1a87aaa7d374205a56b50bae3a8c9136e128f1746d58735dc30c81 + checksum: 10c0/04a967807ef717ed7833083af4b85729635c74e88e9083b242db12617a44c4f9874cf1f15ca3b1c0a024a154ea64867aad69159565965ca5204abe9ca52b9aed languageName: node linkType: hard -"@aztec/native@npm:4.2.0-aztecnr-rc.2": - version: 4.2.0-aztecnr-rc.2 - resolution: "@aztec/native@npm:4.2.0-aztecnr-rc.2" +"@aztec/native@npm:4.2.0-nightly.20260409": + version: 4.2.0-nightly.20260409 + resolution: "@aztec/native@npm:4.2.0-nightly.20260409" dependencies: - "@aztec/bb.js": "npm:4.2.0-aztecnr-rc.2" - "@aztec/foundation": "npm:4.2.0-aztecnr-rc.2" + "@aztec/bb.js": "npm:4.2.0-nightly.20260409" + "@aztec/foundation": "npm:4.2.0-nightly.20260409" msgpackr: "npm:^1.11.2" - checksum: 10c0/baf2d7aff564b3fe4fb713a180671c9cf10e1349f4c90a8bb1c14221c7224ea367c5493160171484cbb749d339a317d8bae0178e9d026c814a5b89e93559e0ea + checksum: 10c0/6a4c456c492e5e3db1006e8c6b2afa205f5e06ec7669630293ddada692949b90ba909e6a5a6432785a26d3fc525768f8275abf9d895c4612097e19daebb136f5 languageName: node linkType: hard -"@aztec/noir-acvm_js@npm:4.2.0-aztecnr-rc.2": - version: 4.2.0-aztecnr-rc.2 - resolution: "@aztec/noir-acvm_js@npm:4.2.0-aztecnr-rc.2" - checksum: 10c0/e3e4d23f128c9100ac0036cb3bfa20f0d3ebaa485badba3af991d1e2efb26a5b8d2df25d3c796a00bbc8459321f8c62af670620552d62591c6ed39d398653614 +"@aztec/noir-acvm_js@npm:4.2.0-nightly.20260409": + version: 4.2.0-nightly.20260409 + resolution: "@aztec/noir-acvm_js@npm:4.2.0-nightly.20260409" + checksum: 10c0/4a37836f14f32b567a869a0544137868afd24b57dd8fda15fd69ebb66dd7d235a3389fcbf56d62d1792ff56c57bb7bbf25b8ed172cbc959f5c1322aad480b27f languageName: node linkType: hard -"@aztec/noir-contracts.js@npm:v4.2.0-aztecnr-rc.2": - version: 4.2.0-aztecnr-rc.2 - resolution: "@aztec/noir-contracts.js@npm:4.2.0-aztecnr-rc.2" +"@aztec/noir-contracts.js@npm:4.2.0-nightly.20260409": + version: 4.2.0-nightly.20260409 + resolution: "@aztec/noir-contracts.js@npm:4.2.0-nightly.20260409" dependencies: - "@aztec/aztec.js": "npm:4.2.0-aztecnr-rc.2" + "@aztec/aztec.js": "npm:4.2.0-nightly.20260409" tslib: "npm:^2.4.0" - checksum: 10c0/6f83d784794ea830e585d4db0328169508832abe56d13ec0826171a58ef92332404200b8dbcd8e4126fd643072c78bf8d55af72d03aff8c15869624a2b9d9dd5 + checksum: 10c0/170e7921583acdb39ef1287512bc9fa2a9f69417167b214d4b44013aa5c1cda3c3654bbd541dbc1e0c20bf3bc292d1bb9a60ed3cbdf6c4631152ecdfe34b9889 languageName: node linkType: hard -"@aztec/noir-noir_codegen@npm:4.2.0-aztecnr-rc.2": - version: 4.2.0-aztecnr-rc.2 - resolution: "@aztec/noir-noir_codegen@npm:4.2.0-aztecnr-rc.2" +"@aztec/noir-noir_codegen@npm:4.2.0-nightly.20260409": + version: 4.2.0-nightly.20260409 + resolution: "@aztec/noir-noir_codegen@npm:4.2.0-nightly.20260409" dependencies: - "@aztec/noir-types": "npm:4.2.0-aztecnr-rc.2" + "@aztec/noir-types": "npm:4.2.0-nightly.20260409" glob: "npm:^13.0.0" ts-command-line-args: "npm:^2.5.1" bin: noir-codegen: lib/main.js - checksum: 10c0/94ae2466e75d7c7d78ea7fe36ecec23bde90489cdedbc56dc5b62d6b789b79e46f78748b8a59cc93ffa7215995ff184ec58fc43e0e96deadb9d241b52a0e6253 + checksum: 10c0/907d182662dc1faa9c8df2ed72baf3f07ef73af2cc6bac3897252249fd0a47d4f22a5cfda0726253c989e394aef77de984e48247686c6d5ccadb93c348127e09 languageName: node linkType: hard -"@aztec/noir-noirc_abi@npm:4.2.0-aztecnr-rc.2": - version: 4.2.0-aztecnr-rc.2 - resolution: "@aztec/noir-noirc_abi@npm:4.2.0-aztecnr-rc.2" +"@aztec/noir-noirc_abi@npm:4.2.0-nightly.20260409": + version: 4.2.0-nightly.20260409 + resolution: "@aztec/noir-noirc_abi@npm:4.2.0-nightly.20260409" dependencies: - "@aztec/noir-types": "npm:4.2.0-aztecnr-rc.2" - checksum: 10c0/987fe64a5c93e433998dcf52c2cbc10889cd7fe6200fb8a8d5ef778d523d64a6af49eafb6a00ea38ab0b6620d9c5e4cc86b678744fd7a84e5ce43528bc53ceb2 + "@aztec/noir-types": "npm:4.2.0-nightly.20260409" + checksum: 10c0/77e27a25d500070cf230fb7aaf747eed46953eeaddbc5ef9537e4c87163f15b24b7fc8a881a84a6b90f043dcfdfa402a2f359e98bd8348fd340fff38c2a297e6 languageName: node linkType: hard -"@aztec/noir-protocol-circuits-types@npm:4.2.0-aztecnr-rc.2": - version: 4.2.0-aztecnr-rc.2 - resolution: "@aztec/noir-protocol-circuits-types@npm:4.2.0-aztecnr-rc.2" +"@aztec/noir-protocol-circuits-types@npm:4.2.0-nightly.20260409": + version: 4.2.0-nightly.20260409 + resolution: "@aztec/noir-protocol-circuits-types@npm:4.2.0-nightly.20260409" dependencies: - "@aztec/blob-lib": "npm:4.2.0-aztecnr-rc.2" - "@aztec/constants": "npm:4.2.0-aztecnr-rc.2" - "@aztec/foundation": "npm:4.2.0-aztecnr-rc.2" - "@aztec/noir-acvm_js": "npm:4.2.0-aztecnr-rc.2" - "@aztec/noir-noir_codegen": "npm:4.2.0-aztecnr-rc.2" - "@aztec/noir-noirc_abi": "npm:4.2.0-aztecnr-rc.2" - "@aztec/noir-types": "npm:4.2.0-aztecnr-rc.2" - "@aztec/stdlib": "npm:4.2.0-aztecnr-rc.2" + "@aztec/blob-lib": "npm:4.2.0-nightly.20260409" + "@aztec/constants": "npm:4.2.0-nightly.20260409" + "@aztec/foundation": "npm:4.2.0-nightly.20260409" + "@aztec/noir-acvm_js": "npm:4.2.0-nightly.20260409" + "@aztec/noir-noir_codegen": "npm:4.2.0-nightly.20260409" + "@aztec/noir-noirc_abi": "npm:4.2.0-nightly.20260409" + "@aztec/noir-types": "npm:4.2.0-nightly.20260409" + "@aztec/stdlib": "npm:4.2.0-nightly.20260409" change-case: "npm:^5.4.4" tslib: "npm:^2.4.0" - checksum: 10c0/993526734bf3b982ede64d56c3f96a416a1431365e7f8ee35be451fe6f79c62509e7939d962a7586035eb43aa1bfd6c7b71ed687936188c7c466d46dd452b209 + checksum: 10c0/b20abeaee9760e56e4ed0061c22c0975288bde3e5873925bd23aca0aa8945190408b510405954870e7ea368f0d9d81591f92d11ad68caac65ca6b7590e562104 languageName: node linkType: hard -"@aztec/noir-types@npm:4.2.0-aztecnr-rc.2": - version: 4.2.0-aztecnr-rc.2 - resolution: "@aztec/noir-types@npm:4.2.0-aztecnr-rc.2" - checksum: 10c0/bced65ec3ea31e2f6e3de903b16427d336941f639858a1b68d42b8983a330685351b3901ec700e2e3487cdbde3bb2db6446b15ecb2d6411f147db9a5a4fd830f +"@aztec/noir-types@npm:4.2.0-nightly.20260409": + version: 4.2.0-nightly.20260409 + resolution: "@aztec/noir-types@npm:4.2.0-nightly.20260409" + checksum: 10c0/2ae30e4bb5da85d2b609c3f9c448f780a21d9140d25fcb705ce7727da5ebdeea14c4cc3751717a3a188e2f143342b13feba483b1d4b44d0b400bbcd637dca6a9 languageName: node linkType: hard -"@aztec/protocol-contracts@npm:4.2.0-aztecnr-rc.2, @aztec/protocol-contracts@npm:v4.2.0-aztecnr-rc.2": - version: 4.2.0-aztecnr-rc.2 - resolution: "@aztec/protocol-contracts@npm:4.2.0-aztecnr-rc.2" +"@aztec/protocol-contracts@npm:4.2.0-nightly.20260409": + version: 4.2.0-nightly.20260409 + resolution: "@aztec/protocol-contracts@npm:4.2.0-nightly.20260409" dependencies: - "@aztec/constants": "npm:4.2.0-aztecnr-rc.2" - "@aztec/foundation": "npm:4.2.0-aztecnr-rc.2" - "@aztec/stdlib": "npm:4.2.0-aztecnr-rc.2" + "@aztec/constants": "npm:4.2.0-nightly.20260409" + "@aztec/foundation": "npm:4.2.0-nightly.20260409" + "@aztec/stdlib": "npm:4.2.0-nightly.20260409" lodash.chunk: "npm:^4.2.0" lodash.omit: "npm:^4.5.0" tslib: "npm:^2.4.0" - checksum: 10c0/935d1fc2ca61bf15ed2c272142d81ca5633942a133860dcd2c62b20a81630c0e8f56cda6a9c50ace10c71b216f4e5c53ad060e3e923fb9fdfbc10f4697ed8df7 - languageName: node - linkType: hard - -"@aztec/pxe@npm:4.2.0-aztecnr-rc.2, @aztec/pxe@npm:v4.2.0-aztecnr-rc.2": - version: 4.2.0-aztecnr-rc.2 - resolution: "@aztec/pxe@npm:4.2.0-aztecnr-rc.2" - dependencies: - "@aztec/bb-prover": "npm:4.2.0-aztecnr-rc.2" - "@aztec/bb.js": "npm:4.2.0-aztecnr-rc.2" - "@aztec/builder": "npm:4.2.0-aztecnr-rc.2" - "@aztec/constants": "npm:4.2.0-aztecnr-rc.2" - "@aztec/ethereum": "npm:4.2.0-aztecnr-rc.2" - "@aztec/foundation": "npm:4.2.0-aztecnr-rc.2" - "@aztec/key-store": "npm:4.2.0-aztecnr-rc.2" - "@aztec/kv-store": "npm:4.2.0-aztecnr-rc.2" - "@aztec/noir-protocol-circuits-types": "npm:4.2.0-aztecnr-rc.2" - "@aztec/noir-types": "npm:4.2.0-aztecnr-rc.2" - "@aztec/protocol-contracts": "npm:4.2.0-aztecnr-rc.2" - "@aztec/simulator": "npm:4.2.0-aztecnr-rc.2" - "@aztec/stdlib": "npm:4.2.0-aztecnr-rc.2" + checksum: 10c0/e26a899cad51766f907d99fb013c80be48d61367a9d1ed7a87dca4f2725b74401c6cdbc429dcb525745799976c77cc64bf5ddb2804709309604fdc975a6cf404 + languageName: node + linkType: hard + +"@aztec/pxe@npm:4.2.0-nightly.20260409": + version: 4.2.0-nightly.20260409 + resolution: "@aztec/pxe@npm:4.2.0-nightly.20260409" + dependencies: + "@aztec/bb-prover": "npm:4.2.0-nightly.20260409" + "@aztec/bb.js": "npm:4.2.0-nightly.20260409" + "@aztec/builder": "npm:4.2.0-nightly.20260409" + "@aztec/constants": "npm:4.2.0-nightly.20260409" + "@aztec/ethereum": "npm:4.2.0-nightly.20260409" + "@aztec/foundation": "npm:4.2.0-nightly.20260409" + "@aztec/key-store": "npm:4.2.0-nightly.20260409" + "@aztec/kv-store": "npm:4.2.0-nightly.20260409" + "@aztec/noir-protocol-circuits-types": "npm:4.2.0-nightly.20260409" + "@aztec/noir-types": "npm:4.2.0-nightly.20260409" + "@aztec/protocol-contracts": "npm:4.2.0-nightly.20260409" + "@aztec/simulator": "npm:4.2.0-nightly.20260409" + "@aztec/stdlib": "npm:4.2.0-nightly.20260409" koa: "npm:^2.16.1" koa-router: "npm:^13.1.1" lodash.omit: "npm:^4.5.0" @@ -981,45 +981,45 @@ __metadata: viem: "npm:@aztec/viem@2.38.2" bin: pxe: dest/bin/index.js - checksum: 10c0/bc697e7fc443a55e58e8d158fb90d627b3b64334831c2923ce97d69e1b13bc55e293910c7c6e1b19f6ff0fa9b16ef73129e9a2168624b8911f985ee883f2a2cc + checksum: 10c0/5f882ffc82c109dd7bbf44bc71bd39706d07c5fa4ff073f6548370d9199a05737c12dd8998f0acf1026a2803dfbfa212a330f383414e5a6e8e10a5be6f72d983 languageName: node linkType: hard -"@aztec/simulator@npm:4.2.0-aztecnr-rc.2": - version: 4.2.0-aztecnr-rc.2 - resolution: "@aztec/simulator@npm:4.2.0-aztecnr-rc.2" +"@aztec/simulator@npm:4.2.0-nightly.20260409": + version: 4.2.0-nightly.20260409 + resolution: "@aztec/simulator@npm:4.2.0-nightly.20260409" dependencies: - "@aztec/constants": "npm:4.2.0-aztecnr-rc.2" - "@aztec/foundation": "npm:4.2.0-aztecnr-rc.2" - "@aztec/native": "npm:4.2.0-aztecnr-rc.2" - "@aztec/noir-acvm_js": "npm:4.2.0-aztecnr-rc.2" - "@aztec/noir-noirc_abi": "npm:4.2.0-aztecnr-rc.2" - "@aztec/noir-protocol-circuits-types": "npm:4.2.0-aztecnr-rc.2" - "@aztec/noir-types": "npm:4.2.0-aztecnr-rc.2" - "@aztec/protocol-contracts": "npm:4.2.0-aztecnr-rc.2" - "@aztec/stdlib": "npm:4.2.0-aztecnr-rc.2" - "@aztec/telemetry-client": "npm:4.2.0-aztecnr-rc.2" - "@aztec/world-state": "npm:4.2.0-aztecnr-rc.2" + "@aztec/constants": "npm:4.2.0-nightly.20260409" + "@aztec/foundation": "npm:4.2.0-nightly.20260409" + "@aztec/native": "npm:4.2.0-nightly.20260409" + "@aztec/noir-acvm_js": "npm:4.2.0-nightly.20260409" + "@aztec/noir-noirc_abi": "npm:4.2.0-nightly.20260409" + "@aztec/noir-protocol-circuits-types": "npm:4.2.0-nightly.20260409" + "@aztec/noir-types": "npm:4.2.0-nightly.20260409" + "@aztec/protocol-contracts": "npm:4.2.0-nightly.20260409" + "@aztec/stdlib": "npm:4.2.0-nightly.20260409" + "@aztec/telemetry-client": "npm:4.2.0-nightly.20260409" + "@aztec/world-state": "npm:4.2.0-nightly.20260409" lodash.clonedeep: "npm:^4.5.0" lodash.merge: "npm:^4.6.2" tslib: "npm:^2.4.0" - checksum: 10c0/b85956e4c34f3d2b1885b663012d7b61d1242ca4c1f907492cd80ca71789f1ed0437c2394413f3fafcdefbcdce2941781a4a43bf84648ed074f305b9695560ab + checksum: 10c0/a03f137aec9370cb5a4844b43fa8d56aa6db43d5022934b6bf13d631e82d1d648389fbfbd5cc5b21784537c2d088a9be4adf02e4e18b749be9f426c8c9780fc7 languageName: node linkType: hard -"@aztec/stdlib@npm:4.2.0-aztecnr-rc.2, @aztec/stdlib@npm:v4.2.0-aztecnr-rc.2": - version: 4.2.0-aztecnr-rc.2 - resolution: "@aztec/stdlib@npm:4.2.0-aztecnr-rc.2" +"@aztec/stdlib@npm:4.2.0-nightly.20260409": + version: 4.2.0-nightly.20260409 + resolution: "@aztec/stdlib@npm:4.2.0-nightly.20260409" dependencies: "@aws-sdk/client-s3": "npm:^3.892.0" - "@aztec/bb.js": "npm:4.2.0-aztecnr-rc.2" - "@aztec/blob-lib": "npm:4.2.0-aztecnr-rc.2" - "@aztec/constants": "npm:4.2.0-aztecnr-rc.2" - "@aztec/ethereum": "npm:4.2.0-aztecnr-rc.2" - "@aztec/foundation": "npm:4.2.0-aztecnr-rc.2" - "@aztec/l1-artifacts": "npm:4.2.0-aztecnr-rc.2" - "@aztec/noir-noirc_abi": "npm:4.2.0-aztecnr-rc.2" - "@aztec/validator-ha-signer": "npm:4.2.0-aztecnr-rc.2" + "@aztec/bb.js": "npm:4.2.0-nightly.20260409" + "@aztec/blob-lib": "npm:4.2.0-nightly.20260409" + "@aztec/constants": "npm:4.2.0-nightly.20260409" + "@aztec/ethereum": "npm:4.2.0-nightly.20260409" + "@aztec/foundation": "npm:4.2.0-nightly.20260409" + "@aztec/l1-artifacts": "npm:4.2.0-nightly.20260409" + "@aztec/noir-noirc_abi": "npm:4.2.0-nightly.20260409" + "@aztec/validator-ha-signer": "npm:4.2.0-nightly.20260409" "@google-cloud/storage": "npm:^7.15.0" axios: "npm:^1.13.5" json-stringify-deterministic: "npm:1.0.12" @@ -1032,16 +1032,16 @@ __metadata: tslib: "npm:^2.4.0" viem: "npm:@aztec/viem@2.38.2" zod: "npm:^3.23.8" - checksum: 10c0/e4c45edfda489b900e75d68c3b84e0d18ebc2b676b4e30b7406fa38f7cd2cda837a68c846677d68b9a7df736fe972ae99cdaae6f626cb5f97d44ce6e453b724d + checksum: 10c0/8465234450908c6ad8c402d0095ecc6e3e6b83dec8c4f635d485e04a3d28ba9d3ecad61e6f497e97ea7e77c9808ddab11e4ef4c51b76398496db30d2ef5c2525 languageName: node linkType: hard -"@aztec/telemetry-client@npm:4.2.0-aztecnr-rc.2": - version: 4.2.0-aztecnr-rc.2 - resolution: "@aztec/telemetry-client@npm:4.2.0-aztecnr-rc.2" +"@aztec/telemetry-client@npm:4.2.0-nightly.20260409": + version: 4.2.0-nightly.20260409 + resolution: "@aztec/telemetry-client@npm:4.2.0-nightly.20260409" dependencies: - "@aztec/foundation": "npm:4.2.0-aztecnr-rc.2" - "@aztec/stdlib": "npm:4.2.0-aztecnr-rc.2" + "@aztec/foundation": "npm:4.2.0-nightly.20260409" + "@aztec/stdlib": "npm:4.2.0-nightly.20260409" "@opentelemetry/api": "npm:^1.9.0" "@opentelemetry/api-logs": "npm:^0.55.0" "@opentelemetry/core": "npm:^1.28.0" @@ -1058,70 +1058,70 @@ __metadata: "@opentelemetry/semantic-conventions": "npm:^1.28.0" prom-client: "npm:^15.1.3" viem: "npm:@aztec/viem@2.38.2" - checksum: 10c0/1db458e6a34d38ca7db1b2967f632a472bbdbaab0ddfd56e2e7136cd5d535396179e27574e2e514d00b2a9dd4dd9e68f14974228df810db319b205b4bbb2bf51 + checksum: 10c0/370129219ea5083ca4a65048b2ead3d128b1621b399ee5d429ff18858fd32d4644f7cc6a95e8b2cdf5e1d227b9dc030375af72ec9b2bbc843cc10ac2991557df languageName: node linkType: hard -"@aztec/validator-ha-signer@npm:4.2.0-aztecnr-rc.2": - version: 4.2.0-aztecnr-rc.2 - resolution: "@aztec/validator-ha-signer@npm:4.2.0-aztecnr-rc.2" +"@aztec/validator-ha-signer@npm:4.2.0-nightly.20260409": + version: 4.2.0-nightly.20260409 + resolution: "@aztec/validator-ha-signer@npm:4.2.0-nightly.20260409" dependencies: - "@aztec/ethereum": "npm:4.2.0-aztecnr-rc.2" - "@aztec/foundation": "npm:4.2.0-aztecnr-rc.2" + "@aztec/ethereum": "npm:4.2.0-nightly.20260409" + "@aztec/foundation": "npm:4.2.0-nightly.20260409" node-pg-migrate: "npm:^8.0.4" pg: "npm:^8.11.3" tslib: "npm:^2.4.0" zod: "npm:^3.23.8" - checksum: 10c0/5029ac2ba6be151a9c1445f5829a1c0d6360b6569a7d875da002303292ebe55f46141a5625a48b6b597da1c366a633a804768b2e5ac0a62f079c4a75b63348df + checksum: 10c0/1b3a7b366d47bd1af6b6f0aa1f015e86a7b63b080c5378ef0dbd0cde1875af0ba48f21cae6797f373f40167dc51178ee1d6be5e0f94bf614a14fa5a7013b5e11 languageName: node linkType: hard -"@aztec/wallet-sdk@npm:4.2.0-aztecnr-rc.2, @aztec/wallet-sdk@npm:v4.2.0-aztecnr-rc.2": - version: 4.2.0-aztecnr-rc.2 - resolution: "@aztec/wallet-sdk@npm:4.2.0-aztecnr-rc.2" +"@aztec/wallet-sdk@npm:4.2.0-nightly.20260409": + version: 4.2.0-nightly.20260409 + resolution: "@aztec/wallet-sdk@npm:4.2.0-nightly.20260409" dependencies: - "@aztec/aztec.js": "npm:4.2.0-aztecnr-rc.2" - "@aztec/constants": "npm:4.2.0-aztecnr-rc.2" - "@aztec/entrypoints": "npm:4.2.0-aztecnr-rc.2" - "@aztec/foundation": "npm:4.2.0-aztecnr-rc.2" - "@aztec/pxe": "npm:4.2.0-aztecnr-rc.2" - "@aztec/stdlib": "npm:4.2.0-aztecnr-rc.2" - checksum: 10c0/15f63c7fb24f03ecedb86818524d966fee3a719751a79cbd4091510ad7853f42fe15291e3b465bcebe0a87d6459eae1a6529e8ee2f361bca1d1fbb6b272d1505 + "@aztec/aztec.js": "npm:4.2.0-nightly.20260409" + "@aztec/constants": "npm:4.2.0-nightly.20260409" + "@aztec/entrypoints": "npm:4.2.0-nightly.20260409" + "@aztec/foundation": "npm:4.2.0-nightly.20260409" + "@aztec/pxe": "npm:4.2.0-nightly.20260409" + "@aztec/stdlib": "npm:4.2.0-nightly.20260409" + checksum: 10c0/620fe6900382480b53369a70ba07de714a9db4d40f9bdb6723cea560a3887e95ca50d9e7b2d9966ae799cef4b6321da7b20f038b5bc745553f1e4ce39e3ca2fd languageName: node linkType: hard -"@aztec/wallets@npm:v4.2.0-aztecnr-rc.2": - version: 4.2.0-aztecnr-rc.2 - resolution: "@aztec/wallets@npm:4.2.0-aztecnr-rc.2" +"@aztec/wallets@npm:4.2.0-nightly.20260409": + version: 4.2.0-nightly.20260409 + resolution: "@aztec/wallets@npm:4.2.0-nightly.20260409" dependencies: - "@aztec/accounts": "npm:4.2.0-aztecnr-rc.2" - "@aztec/aztec.js": "npm:4.2.0-aztecnr-rc.2" - "@aztec/entrypoints": "npm:4.2.0-aztecnr-rc.2" - "@aztec/foundation": "npm:4.2.0-aztecnr-rc.2" - "@aztec/kv-store": "npm:4.2.0-aztecnr-rc.2" - "@aztec/protocol-contracts": "npm:4.2.0-aztecnr-rc.2" - "@aztec/pxe": "npm:4.2.0-aztecnr-rc.2" - "@aztec/stdlib": "npm:4.2.0-aztecnr-rc.2" - "@aztec/wallet-sdk": "npm:4.2.0-aztecnr-rc.2" - checksum: 10c0/f0946f002e52f55752618f876fd70f74d7230cbbc996b68ef8c0fb7cd238847f2e18128cb8fc9dbd3122468f28d34a26e0bb262d80cc1870eeb4ec96a52478b6 + "@aztec/accounts": "npm:4.2.0-nightly.20260409" + "@aztec/aztec.js": "npm:4.2.0-nightly.20260409" + "@aztec/entrypoints": "npm:4.2.0-nightly.20260409" + "@aztec/foundation": "npm:4.2.0-nightly.20260409" + "@aztec/kv-store": "npm:4.2.0-nightly.20260409" + "@aztec/protocol-contracts": "npm:4.2.0-nightly.20260409" + "@aztec/pxe": "npm:4.2.0-nightly.20260409" + "@aztec/stdlib": "npm:4.2.0-nightly.20260409" + "@aztec/wallet-sdk": "npm:4.2.0-nightly.20260409" + checksum: 10c0/c9b9c7069335c0f622b8e08a48195ccfab657afe140ed5c2ec1c544282e9d07a1414d1c0160a98a658147f6452493706b4baa8d8f9da9ab4c9116b67d570cb70 languageName: node linkType: hard -"@aztec/world-state@npm:4.2.0-aztecnr-rc.2": - version: 4.2.0-aztecnr-rc.2 - resolution: "@aztec/world-state@npm:4.2.0-aztecnr-rc.2" +"@aztec/world-state@npm:4.2.0-nightly.20260409": + version: 4.2.0-nightly.20260409 + resolution: "@aztec/world-state@npm:4.2.0-nightly.20260409" dependencies: - "@aztec/constants": "npm:4.2.0-aztecnr-rc.2" - "@aztec/foundation": "npm:4.2.0-aztecnr-rc.2" - "@aztec/kv-store": "npm:4.2.0-aztecnr-rc.2" - "@aztec/merkle-tree": "npm:4.2.0-aztecnr-rc.2" - "@aztec/native": "npm:4.2.0-aztecnr-rc.2" - "@aztec/protocol-contracts": "npm:4.2.0-aztecnr-rc.2" - "@aztec/stdlib": "npm:4.2.0-aztecnr-rc.2" - "@aztec/telemetry-client": "npm:4.2.0-aztecnr-rc.2" + "@aztec/constants": "npm:4.2.0-nightly.20260409" + "@aztec/foundation": "npm:4.2.0-nightly.20260409" + "@aztec/kv-store": "npm:4.2.0-nightly.20260409" + "@aztec/merkle-tree": "npm:4.2.0-nightly.20260409" + "@aztec/native": "npm:4.2.0-nightly.20260409" + "@aztec/protocol-contracts": "npm:4.2.0-nightly.20260409" + "@aztec/stdlib": "npm:4.2.0-nightly.20260409" + "@aztec/telemetry-client": "npm:4.2.0-nightly.20260409" tslib: "npm:^2.4.0" zod: "npm:^3.23.8" - checksum: 10c0/45435b414c4692267c082e2d41363150b7d8043804134d9c260e702ea0a346ad58d16bde55dcbdf30823f24a4f6135857dd7b4dac6612a2f093640262ba4b077 + checksum: 10c0/fc16d2974ea9ce502060ae1ce82010084ad30e2e307d4c115566eea2807cc76b0db8c89f972a7fb35fc72b40ae54593635bb2ca0c461e68fd8494877b6e84531 languageName: node linkType: hard @@ -1773,32 +1773,30 @@ __metadata: languageName: node linkType: hard -"@gregojuice/contracts@npm:^0.0.10": - version: 0.0.10 - resolution: "@gregojuice/contracts@npm:0.0.10" +"@gregojuice/contracts@portal:/mnt/user-data/martin/gregojuice/packages/contracts::locator=gregoswap%40workspace%3A.": + version: 0.0.0-use.local + resolution: "@gregojuice/contracts@portal:/mnt/user-data/martin/gregojuice/packages/contracts::locator=gregoswap%40workspace%3A." dependencies: - "@aztec/aztec.js": "npm:v4.2.0-aztecnr-rc.2" - "@aztec/foundation": "npm:v4.2.0-aztecnr-rc.2" - "@aztec/stdlib": "npm:v4.2.0-aztecnr-rc.2" - "@aztec/wallets": "npm:v4.2.0-aztecnr-rc.2" - checksum: 10c0/a4cffe25a1fc1e5e9eb77659205fd23fdd47464c706b36051bd8823862974b4cb0e9a77598fafd6c93cbde059a5e8eb3b90fc4680f4ed83973ea22e5b9e73b80 + "@aztec/aztec.js": "npm:4.2.0-nightly.20260409" + "@aztec/foundation": "npm:4.2.0-nightly.20260409" + "@aztec/stdlib": "npm:4.2.0-nightly.20260409" + "@aztec/wallets": "npm:4.2.0-nightly.20260409" languageName: node - linkType: hard + linkType: soft -"@gregojuice/embedded-wallet@npm:^0.0.10": - version: 0.0.10 - resolution: "@gregojuice/embedded-wallet@npm:0.0.10" +"@gregojuice/embedded-wallet@portal:/mnt/user-data/martin/gregojuice/packages/embedded-wallet::locator=gregoswap%40workspace%3A.": + version: 0.0.0-use.local + resolution: "@gregojuice/embedded-wallet@portal:/mnt/user-data/martin/gregojuice/packages/embedded-wallet::locator=gregoswap%40workspace%3A." dependencies: - "@aztec/aztec.js": "npm:v4.2.0-aztecnr-rc.2" - "@aztec/entrypoints": "npm:v4.2.0-aztecnr-rc.2" - "@aztec/foundation": "npm:v4.2.0-aztecnr-rc.2" - "@aztec/pxe": "npm:v4.2.0-aztecnr-rc.2" - "@aztec/stdlib": "npm:v4.2.0-aztecnr-rc.2" - "@aztec/wallet-sdk": "npm:v4.2.0-aztecnr-rc.2" - "@aztec/wallets": "npm:v4.2.0-aztecnr-rc.2" - checksum: 10c0/7ec0b41494ace424688f0d3ff8060429b150a438fe7a5a78bb857a14f53384d3fa07969911896dcfca4ebb8224492c05b518417e91e618f21f6581be9a2cba42 + "@aztec/aztec.js": "npm:4.2.0-nightly.20260409" + "@aztec/entrypoints": "npm:4.2.0-nightly.20260409" + "@aztec/foundation": "npm:4.2.0-nightly.20260409" + "@aztec/pxe": "npm:4.2.0-nightly.20260409" + "@aztec/stdlib": "npm:4.2.0-nightly.20260409" + "@aztec/wallet-sdk": "npm:4.2.0-nightly.20260409" + "@aztec/wallets": "npm:4.2.0-nightly.20260409" languageName: node - linkType: hard + linkType: soft "@hapi/bourne@npm:^3.0.0": version: 3.0.0 @@ -6071,22 +6069,22 @@ __metadata: version: 0.0.0-use.local resolution: "gregoswap@workspace:." dependencies: - "@aztec/accounts": "npm:v4.2.0-aztecnr-rc.2" - "@aztec/aztec.js": "npm:v4.2.0-aztecnr-rc.2" - "@aztec/constants": "npm:v4.2.0-aztecnr-rc.2" - "@aztec/entrypoints": "npm:v4.2.0-aztecnr-rc.2" - "@aztec/foundation": "npm:v4.2.0-aztecnr-rc.2" - "@aztec/noir-contracts.js": "npm:v4.2.0-aztecnr-rc.2" - "@aztec/protocol-contracts": "npm:v4.2.0-aztecnr-rc.2" - "@aztec/pxe": "npm:v4.2.0-aztecnr-rc.2" - "@aztec/stdlib": "npm:v4.2.0-aztecnr-rc.2" - "@aztec/wallet-sdk": "npm:v4.2.0-aztecnr-rc.2" - "@aztec/wallets": "npm:v4.2.0-aztecnr-rc.2" + "@aztec/accounts": "npm:4.2.0-nightly.20260409" + "@aztec/aztec.js": "npm:4.2.0-nightly.20260409" + "@aztec/constants": "npm:4.2.0-nightly.20260409" + "@aztec/entrypoints": "npm:4.2.0-nightly.20260409" + "@aztec/foundation": "npm:4.2.0-nightly.20260409" + "@aztec/noir-contracts.js": "npm:4.2.0-nightly.20260409" + "@aztec/protocol-contracts": "npm:4.2.0-nightly.20260409" + "@aztec/pxe": "npm:4.2.0-nightly.20260409" + "@aztec/stdlib": "npm:4.2.0-nightly.20260409" + "@aztec/wallet-sdk": "npm:4.2.0-nightly.20260409" + "@aztec/wallets": "npm:4.2.0-nightly.20260409" "@emotion/react": "npm:^11.14.0" "@emotion/styled": "npm:^11.14.0" "@eslint/js": "npm:^9.18.0" - "@gregojuice/contracts": "npm:^0.0.10" - "@gregojuice/embedded-wallet": "npm:^0.0.10" + "@gregojuice/contracts": "portal:/mnt/user-data/martin/gregojuice/packages/contracts" + "@gregojuice/embedded-wallet": "portal:/mnt/user-data/martin/gregojuice/packages/embedded-wallet" "@mui/icons-material": "npm:^6.3.1" "@mui/material": "npm:^6.3.1" "@mui/styles": "npm:^6.3.1" From 4e6ce3adde25614f910780bde5660d208e4a2dd5 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Thu, 9 Apr 2026 12:21:01 +0000 Subject: [PATCH 02/24] Fork token contract and point AMM/PoP deps to local copy Copies the Aztec standard token contract into contracts/token/, removes the test module, and adds a transfer_offchain method that delivers notes via MessageDelivery.OFFCHAIN. Updates AMM and ProofOfPassword to depend on this local fork instead of the upstream git reference. Co-Authored-By: Claude Sonnet 4.6 --- contracts/amm/Nargo.toml | 6 +- contracts/proof_of_password/Nargo.toml | 6 +- contracts/token/Nargo.toml | 10 + contracts/token/src/main.nr | 545 +++++++++++++++++++++++++ 4 files changed, 561 insertions(+), 6 deletions(-) create mode 100644 contracts/token/Nargo.toml create mode 100644 contracts/token/src/main.nr diff --git a/contracts/amm/Nargo.toml b/contracts/amm/Nargo.toml index a3dde64..eda0081 100644 --- a/contracts/amm/Nargo.toml +++ b/contracts/amm/Nargo.toml @@ -4,6 +4,6 @@ authors = [""] type = "contract" [dependencies] -aztec = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-nightly.20260409", directory = "noir-projects/aztec-nr/aztec" } -token = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-nightly.20260409", directory = "noir-projects/noir-contracts/contracts/app/token_contract" } -uint_note = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-nightly.20260409", directory = "noir-projects/aztec-nr/uint-note" } +aztec = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-nightly.20260410", directory = "noir-projects/aztec-nr/aztec" } +token = { path = "../token" } +uint_note = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-nightly.20260410", directory = "noir-projects/aztec-nr/uint-note" } diff --git a/contracts/proof_of_password/Nargo.toml b/contracts/proof_of_password/Nargo.toml index 828567e..8c3eba9 100644 --- a/contracts/proof_of_password/Nargo.toml +++ b/contracts/proof_of_password/Nargo.toml @@ -4,7 +4,7 @@ type = "contract" authors = [""] [dependencies] -aztec = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-nightly.20260409", directory = "noir-projects/aztec-nr/aztec" } -token = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-nightly.20260409", directory = "noir-projects/noir-contracts/contracts/app/token_contract" } +aztec = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-nightly.20260410", directory = "noir-projects/aztec-nr/aztec" } +token = { path = "../token" } poseidon = { tag = "v0.1.1", git = "https://github.com/noir-lang/poseidon" } -compressed_string = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-nightly.20260409", directory = "noir-projects/aztec-nr/compressed-string" } \ No newline at end of file +compressed_string = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-nightly.20260410", directory = "noir-projects/aztec-nr/compressed-string" } diff --git a/contracts/token/Nargo.toml b/contracts/token/Nargo.toml new file mode 100644 index 0000000..8cd6e38 --- /dev/null +++ b/contracts/token/Nargo.toml @@ -0,0 +1,10 @@ +[package] +name = "token_contract" +authors = [""] +type = "contract" + +[dependencies] +aztec = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-aztecnr-rc.2", directory = "noir-projects/aztec-nr/aztec" } +uint_note = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-aztecnr-rc.2", directory = "noir-projects/aztec-nr/uint-note" } +compressed_string = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-aztecnr-rc.2", directory = "noir-projects/aztec-nr/compressed-string" } +balance_set = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-aztecnr-rc.2", directory = "noir-projects/aztec-nr/balance-set" } diff --git a/contracts/token/src/main.nr b/contracts/token/src/main.nr new file mode 100644 index 0000000..0ee715d --- /dev/null +++ b/contracts/token/src/main.nr @@ -0,0 +1,545 @@ +// docs:start:imports +use aztec::macros::aztec; + +// Minimal token implementation that supports `AuthWit` accounts. +// The auth message follows a similar pattern to the cross-chain message and includes a designated caller. +// The designated caller is ALWAYS used here, and not based on a flag as cross-chain. +// message hash = H([caller, contract, selector, ...args]) +// To be read as `caller` calls function at `contract` defined by `selector` with `args` +// Including a nonce in the message hash ensures that the message can only be used once. +#[aztec] +pub contract Token { + // Libs + use std::ops::{Add, Sub}; + + use compressed_string::FieldCompressedString; + + use aztec::{ + authwit::auth::compute_authwit_nullifier, + context::{PrivateCall, PrivateContext}, + macros::{ + events::event, + functions::{authorize_once, external, initializer, internal, only_self, view}, + storage::storage, + }, + messages::message_delivery::MessageDelivery, + protocol::{address::AztecAddress, traits::ToField}, + state_vars::{Map, Owned, PublicImmutable, PublicMutable, StateVariable}, + }; + + use uint_note::{PartialUintNote, UintNote}; + + use balance_set::BalanceSet; + + // docs:end::imports + + // In the first transfer iteration we are computing a lot of additional information (validating inputs, retrieving + // keys, etc.), so the gate count is already relatively high. We therefore only read a few notes to keep the happy + // case with few constraints. + global INITIAL_TRANSFER_CALL_MAX_NOTES: u32 = 2; + // All the recursive call does is nullify notes, meaning the gate count is low, but it is all constant overhead. We + // therefore read more notes than in the base case to increase the efficiency of the overhead, since this results in + // an overall small circuit regardless. + global RECURSIVE_TRANSFER_CALL_MAX_NOTES: u32 = 8; + + #[event] + struct Transfer { + from: AztecAddress, + to: AztecAddress, + amount: u128, + } + + // docs:start:storage_struct + #[storage] + struct Storage { + admin: PublicMutable, + minters: Map, Context>, + balances: Owned, Context>, + total_supply: PublicMutable, + public_balances: Map, Context>, + symbol: PublicImmutable, + name: PublicImmutable, + decimals: PublicImmutable, + } + // docs:end:storage_struct + + // docs:start:constructor + #[external("public")] + #[initializer] + fn constructor(admin: AztecAddress, name: str<31>, symbol: str<31>, decimals: u8) { + assert(!admin.is_zero(), "invalid admin"); + self.storage.admin.write(admin); + self.storage.minters.at(admin).write(true); + self.storage.name.initialize(FieldCompressedString::from_string(name)); + self.storage.symbol.initialize(FieldCompressedString::from_string(symbol)); + self.storage.decimals.initialize(decimals); + } + // docs:end:constructor + + #[external("public")] + fn set_admin(new_admin: AztecAddress) { + assert(self.storage.admin.read().eq(self.msg_sender()), "caller is not admin"); + self.storage.admin.write(new_admin); + } + + // docs:start:public_immutable_read + #[external("public")] + #[view] + fn public_get_name() -> FieldCompressedString { + self.storage.name.read() + } + + #[external("private")] + #[view] + fn private_get_name() -> FieldCompressedString { + self.storage.name.read() + } + // docs:end:public_immutable_read + + #[external("public")] + #[view] + fn public_get_symbol() -> pub FieldCompressedString { + self.storage.symbol.read() + } + + #[external("private")] + #[view] + fn private_get_symbol() -> pub FieldCompressedString { + self.storage.symbol.read() + } + + #[external("public")] + #[view] + fn public_get_decimals() -> pub u8 { + self.storage.decimals.read() + } + + #[external("private")] + #[view] + fn private_get_decimals() -> pub u8 { + self.storage.decimals.read() + } + + #[external("public")] + #[view] + fn get_admin() -> Field { + self.storage.admin.read().to_field() + } + + #[external("public")] + #[view] + fn is_minter(minter: AztecAddress) -> bool { + self.storage.minters.at(minter).read() + } + + #[external("public")] + #[view] + fn total_supply() -> u128 { + self.storage.total_supply.read() + } + + #[external("public")] + #[view] + fn balance_of_public(owner: AztecAddress) -> u128 { + self.storage.public_balances.at(owner).read() + } + + // docs:start:set_minter + #[external("public")] + fn set_minter(minter: AztecAddress, approve: bool) { + assert(self.storage.admin.read().eq(self.msg_sender()), "caller is not admin"); + self.storage.minters.at(minter).write(approve); + } + // docs:end:set_minter + + #[external("public")] + fn mint_to_public(to: AztecAddress, amount: u128) { + assert(self.storage.minters.at(self.msg_sender()).read(), "caller is not minter"); + let new_balance = self.storage.public_balances.at(to).read().add(amount); + let supply = self.storage.total_supply.read().add(amount); + self.storage.public_balances.at(to).write(new_balance); + self.storage.total_supply.write(supply); + } + + // docs:start:transfer_in_public + #[authorize_once("from", "authwit_nonce")] + #[external("public")] + fn transfer_in_public( + from: AztecAddress, + to: AztecAddress, + amount: u128, + authwit_nonce: Field, + ) { + let from_balance = self.storage.public_balances.at(from).read().sub(amount); + self.storage.public_balances.at(from).write(from_balance); + let to_balance = self.storage.public_balances.at(to).read().add(amount); + self.storage.public_balances.at(to).write(to_balance); + } + // docs:end:transfer_in_public + + #[authorize_once("from", "authwit_nonce")] + #[external("public")] + fn burn_public(from: AztecAddress, amount: u128, authwit_nonce: Field) { + let from_balance = self.storage.public_balances.at(from).read().sub(amount); + self.storage.public_balances.at(from).write(from_balance); + let new_supply = self.storage.total_supply.read().sub(amount); + self.storage.total_supply.write(new_supply); + } + + #[authorize_once("from", "authwit_nonce")] + #[external("private")] + fn transfer_to_public( + from: AztecAddress, + to: AztecAddress, + amount: u128, + authwit_nonce: Field, + ) { + self.storage.balances.at(from).sub(amount).deliver(MessageDelivery.ONCHAIN_CONSTRAINED); + self.enqueue_self._increase_public_balance(to, amount); + } + + /// Transfers tokens from private balance of `from` to public balance of `to` and prepares a partial note for + /// receiving change for `from`. + /// + /// This is an optimization that combines two operations into one to reduce contract calls: + /// 1. Transfers `amount` tokens from `from`'s private balance to `to`'s public balance + /// 2. Creates a partial note that can later be used to receive change back to `from`'s private balance + /// + /// This pattern is useful when interacting with contracts that: + /// - Receive tokens from a user's private balance + /// - Need to wait until public execution to determine how many tokens to return (e.g. AMM, FPC) + /// - Will return tokens to the user's private balance + /// + /// The contract can use the returned partial note to complete the transfer back to private + /// once the final amount is known during public execution. + #[authorize_once("from", "authwit_nonce")] + #[external("private")] + fn transfer_to_public_and_prepare_private_balance_increase( + from: AztecAddress, + to: AztecAddress, + amount: u128, + authwit_nonce: Field, + ) -> PartialUintNote { + self.storage.balances.at(from).sub(amount).deliver(MessageDelivery.ONCHAIN_CONSTRAINED); + self.enqueue_self._increase_public_balance(to, amount); + + // We prepare the private balance increase (the partial note for the change). + self.internal._prepare_private_balance_increase(from) + } + + #[external("private")] + fn transfer(to: AztecAddress, amount: u128) { + let from = self.msg_sender(); + + // We reduce `from`'s balance by amount by recursively removing notes over potentially multiple calls. This + // method keeps the gate count for each individual call low - reading too many notes at once could result in + // circuits in which proving is not feasible. + // Since the sum of the amounts in the notes we nullified was potentially larger than amount, we create a new + // note for `from` with the change amount, e.g. if `amount` is 10 and two notes are nullified with amounts 8 and + // 5, then the change will be 3 (since 8 + 5 - 10 = 3). + let change = self.internal.subtract_balance(from, amount, INITIAL_TRANSFER_CALL_MAX_NOTES); + self.storage.balances.at(from).add(change).deliver(MessageDelivery.ONCHAIN_UNCONSTRAINED); + self.storage.balances.at(to).add(amount).deliver(MessageDelivery.ONCHAIN_UNCONSTRAINED); + + // We don't constrain encryption of the note log in `transfer` (unlike in `transfer_in_private`) because the transfer + // function is only designed to be used in situations where the event is not strictly necessary (e.g. payment to + // another person where the payment is considered to be successful when the other party successfully decrypts a + // note). + self.emit(Transfer { from, to, amount }).deliver_to( + to, + MessageDelivery.ONCHAIN_UNCONSTRAINED, + ); + } + + #[external("private")] + fn transfer_offchain(to: AztecAddress, amount: u128) { + let from = self.msg_sender(); + + let change = self.internal.subtract_balance(from, amount, INITIAL_TRANSFER_CALL_MAX_NOTES); + self.storage.balances.at(from).add(change).deliver(MessageDelivery.OFFCHAIN); + self.storage.balances.at(to).add(amount).deliver(MessageDelivery.OFFCHAIN); + + self.emit(Transfer { from, to, amount }).deliver_to( + to, + MessageDelivery.OFFCHAIN, + ); + } + + #[internal("private")] + fn subtract_balance(account: AztecAddress, amount: u128, max_notes: u32) -> u128 { + let subtracted = self.storage.balances.at(account).try_sub(amount, max_notes); + // Failing to subtract any amount means that the owner was unable to produce more notes that could be nullified. + // We could in some cases fail early inside try_sub if we detected that fewer notes than the maximum were + // returned and we were still unable to reach the target amount, but that'd make the code more complicated, and + // optimizing for the failure scenario is not as important. + assert(subtracted > 0 as u128, "Balance too low"); + if subtracted >= amount { + // We have achieved our goal of nullifying notes that add up to more than amount, so we return the change + subtracted - amount + } else { + // try_sub failed to nullify enough notes to reach the target amount, so we compute the amount remaining + // and try again. + let remaining = amount - subtracted; + compute_recurse_subtract_balance_call(*context, account, remaining).call(context) + } + } + + // TODO(#7729): apply no_predicates to the contract interface method directly instead of having to use a wrapper + // like we do here. + #[no_predicates] + #[contract_library_method] + fn compute_recurse_subtract_balance_call( + context: PrivateContext, + account: AztecAddress, + remaining: u128, + ) -> PrivateCall<25, 2, u128> { + Token::at(context.this_address())._recurse_subtract_balance(account, remaining) + } + + #[only_self] + #[external("private")] + fn _recurse_subtract_balance(account: AztecAddress, amount: u128) -> u128 { + self.internal.subtract_balance(account, amount, RECURSIVE_TRANSFER_CALL_MAX_NOTES) + } + + /** + * Cancel a private authentication witness. + * @param inner_hash The inner hash of the authwit to cancel. + */ + // docs:start:cancel_authwit + #[external("private")] + fn cancel_authwit(inner_hash: Field) { + let on_behalf_of = self.msg_sender(); + let nullifier = compute_authwit_nullifier(on_behalf_of, inner_hash); + self.context.push_nullifier(nullifier); + } + // docs:end:cancel_authwit + + // docs:start:transfer_in_private + #[authorize_once("from", "authwit_nonce")] + #[external("private")] + fn transfer_in_private( + from: AztecAddress, + to: AztecAddress, + amount: u128, + authwit_nonce: Field, + ) { + // docs:start:increase_private_balance + self.storage.balances.at(from).sub(amount).deliver(MessageDelivery.ONCHAIN_CONSTRAINED); + // docs:end:increase_private_balance + self.storage.balances.at(to).add(amount).deliver(MessageDelivery.ONCHAIN_CONSTRAINED); + } + // docs:end:transfer_in_private + + #[authorize_once("from", "authwit_nonce")] + #[external("private")] + fn burn_private(from: AztecAddress, amount: u128, authwit_nonce: Field) { + self.storage.balances.at(from).sub(amount).deliver(MessageDelivery.ONCHAIN_CONSTRAINED); + self.enqueue_self._reduce_total_supply(amount); + } + + // Transfers token `amount` from public balance of message sender to a private balance of `to`. + #[external("private")] + fn transfer_to_private(to: AztecAddress, amount: u128) { + // `from` is the owner of the public balance from which we'll subtract the `amount`. + let from = self.msg_sender(); + + // We prepare the private balance increase (the partial note). + let partial_note = self.internal._prepare_private_balance_increase(to); + + // At last we finalize the transfer. Usage of the `unsafe` method here is safe because we set the `from` + // function argument to a message sender, guaranteeing that he can transfer only his own tokens. + self.enqueue_self._finalize_transfer_to_private_unsafe(from, amount, partial_note); + } + + /// Prepares an increase of private balance of `to` (partial note). The increase needs to be finalized by calling + /// some of the finalization functions (`finalize_transfer_to_private`, `finalize_mint_to_private`) with the + /// returned partial note. + #[external("private")] + fn prepare_private_balance_increase(to: AztecAddress) -> PartialUintNote { + self.internal._prepare_private_balance_increase(to) + } + + /// This function exists separately from `prepare_private_balance_increase` solely as an optimization as it allows + /// us to have it inlined in the `transfer_to_private` function which results in one fewer kernel iteration. Note + /// that in this case we don't pass `completer` as an argument to this function because in all the callsites we + /// want to use the message sender as the completer anyway. + // docs:start:prepare_private_balance_increase + #[internal("private")] + fn _prepare_private_balance_increase(to: AztecAddress) -> PartialUintNote { + let partial_note = UintNote::partial(to, self.context, to, self.msg_sender()); + + partial_note + } + // docs:end:prepare_private_balance_increase + + /// Finalizes a transfer of token `amount` from public balance of `msg_sender` to a private balance of `to`. + /// The transfer must be prepared by calling `prepare_private_balance_increase` from `msg_sender` account and + /// the resulting `partial_note` must be passed as an argument to this function. + /// + /// Note that this contract does not protect against a `partial_note` being used multiple times and it is up to + /// the caller of this function to ensure that it doesn't happen. If the same `partial_note` is used multiple + /// times, the token `amount` would most likely get lost (the partial note log processing functionality would fail + /// to find the pending partial note when trying to complete it). + #[external("public")] + fn finalize_transfer_to_private(amount: u128, partial_note: PartialUintNote) { + // Completer is the entity that can complete the partial note. In this case, it's the same as the account + // `from` from whose balance we're subtracting the `amount`. + let from_and_completer = self.msg_sender(); + self.internal._finalize_transfer_to_private(from_and_completer, amount, partial_note); + } + + /// Finalizes a transfer of token `amount` from private balance of `from` to a private balance of `to`. + /// The transfer must be prepared by calling `prepare_private_balance_increase` from `from` account and + /// the resulting `partial_note` must be passed as an argument to this function. + /// + /// Note that this contract does not protect against a `partial_note` being used multiple times and it is up to + /// the caller of this function to ensure that it doesn't happen. If the same `partial_note` is used multiple + /// times, the token `amount` would most likely get lost (the partial note log processing functionality would fail + /// to find the pending partial note when trying to complete it). + #[authorize_once("from", "authwit_nonce")] + #[external("private")] + fn finalize_transfer_to_private_from_private( + from: AztecAddress, + partial_note: PartialUintNote, + amount: u128, + authwit_nonce: Field, + ) { + // First we subtract the `amount` from the private balance of `from` + self.storage.balances.at(from).sub(amount).deliver(MessageDelivery.ONCHAIN_CONSTRAINED); + + partial_note.complete_from_private( + self.context, + self.msg_sender(), + self.storage.balances.get_storage_slot(), + amount, + ); + } + + /// This is a wrapper around `_finalize_transfer_to_private` placed here so that a call + /// to `_finalize_transfer_to_private` can be enqueued. Called unsafe as it does not check `from_and_completer` + /// (this has to be done in the calling function). + #[external("public")] + #[only_self] + fn _finalize_transfer_to_private_unsafe( + from_and_completer: AztecAddress, + amount: u128, + partial_note: PartialUintNote, + ) { + self.internal._finalize_transfer_to_private(from_and_completer, amount, partial_note); + } + + // In all the flows in this contract, `from` (the account from which we're subtracting the `amount`) and + // `completer` (the entity that can complete the partial note) are the same so we represent them with a single + // argument. + // docs:start:finalize_transfer_to_private + #[internal("public")] + fn _finalize_transfer_to_private( + from_and_completer: AztecAddress, + amount: u128, + partial_note: PartialUintNote, + ) { + // First we subtract the `amount` from the public balance of `from_and_completer` + let balance_storage = self.storage.public_balances.at(from_and_completer); + + let from_balance = balance_storage.read().sub(amount); + balance_storage.write(from_balance); + + // We finalize the transfer by completing the partial note. + partial_note.complete( + self.context, + from_and_completer, + self.storage.balances.get_storage_slot(), + amount, + ); + } + // docs:end:finalize_transfer_to_private + + /// Mints token `amount` to a private balance of `to`. Message sender has to have minter permissions (checked + /// in the enqueued call). + #[external("private")] + fn mint_to_private(to: AztecAddress, amount: u128) { + // We prepare the partial note to which we'll "send" the minted amount. + let partial_note = self.internal._prepare_private_balance_increase(to); + + // At last we finalize the mint. Usage of the `unsafe` method here is safe because we set + // the `minter_and_completer` function argument to a message sender, guaranteeing that only a message sender + // with minter permissions can successfully execute the function. + self.enqueue_self._finalize_mint_to_private_unsafe(self.msg_sender(), amount, partial_note); + } + + /// Finalizes a mint of token `amount` to a private balance of `to`. The mint must be prepared by calling + /// `prepare_private_balance_increase` first and the resulting + /// `partial_note` must be passed as an argument to this function. + /// + /// Note: This function is only an optimization as it could be replaced by a combination of `mint_to_public` + /// and `finalize_transfer_to_private`. It is however used very commonly so it makes sense to optimize it + /// (e.g. used during token bridging, in AMM liquidity token etc.). + #[external("public")] + fn finalize_mint_to_private(amount: u128, partial_note: PartialUintNote) { + // Completer is the entity that can complete the partial note. In this case, it's the same as the minter + // account. + let minter_and_completer = self.msg_sender(); + assert(self.storage.minters.at(minter_and_completer).read(), "caller is not minter"); + + self.internal._finalize_mint_to_private(minter_and_completer, amount, partial_note); + } + + /// This is a wrapper around `_finalize_mint_to_private` placed here so that a call + /// to `_finalize_mint_to_private` can be enqueued. Called unsafe as it does not check `minter_and_completer` (this + /// has to be done in the calling function). + #[external("public")] + #[only_self] + fn _finalize_mint_to_private_unsafe( + minter_and_completer: AztecAddress, + amount: u128, + partial_note: PartialUintNote, + ) { + // We check the minter permissions as it was not done in `mint_to_private` function. + assert(self.storage.minters.at(minter_and_completer).read(), "caller is not minter"); + self.internal._finalize_mint_to_private(minter_and_completer, amount, partial_note); + } + + #[internal("public")] + fn _finalize_mint_to_private( + completer: AztecAddress, // entity that can complete the partial note + amount: u128, + partial_note: PartialUintNote, + ) { + // First we increase the total supply by the `amount` + let supply = self.storage.total_supply.read().add(amount); + self.storage.total_supply.write(supply); + + // We finalize the transfer by completing the partial note. + partial_note.complete( + self.context, + completer, + self.storage.balances.get_storage_slot(), + amount, + ); + } + + #[external("public")] + #[only_self] + fn _increase_public_balance(to: AztecAddress, amount: u128) { + let to_balance = self.storage.public_balances.at(to); + + let new_balance = to_balance.read().add(amount); + to_balance.write(new_balance); + } + + #[external("public")] + #[only_self] + fn _reduce_total_supply(amount: u128) { + // Only to be called from burn. + let new_supply = self.storage.total_supply.read().sub(amount); + self.storage.total_supply.write(new_supply); + } + + // docs:start:balance_of_private + #[external("utility")] + unconstrained fn balance_of_private(owner: AztecAddress) -> u128 { + self.storage.balances.at(owner).balance_of() + } + // docs:end:balance_of_private +} From 551e53d68a4b18bfdabacc0e5b84a082e856712d Mon Sep 17 00:00:00 2001 From: mverzilli Date: Thu, 9 Apr 2026 12:22:16 +0000 Subject: [PATCH 03/24] feat: add offchain link service for encoding/decoding transfer URLs --- src/services/offchainLinkService.ts | 46 +++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 src/services/offchainLinkService.ts diff --git a/src/services/offchainLinkService.ts b/src/services/offchainLinkService.ts new file mode 100644 index 0000000..17d47df --- /dev/null +++ b/src/services/offchainLinkService.ts @@ -0,0 +1,46 @@ +/** + * Offchain Link Service + * Encodes/decodes offchain transfer messages into shareable URLs + */ + +export interface TransferLink { + token: 'gc' | 'gcp'; + amount: string; + recipient: string; + contractAddress: string; + txHash: string; + anchorBlockTimestamp: string; + payload: string[]; +} + +export function encodeTransferLink(data: TransferLink): string { + const json = JSON.stringify(data); + const encoded = btoa(json) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); + return `${window.location.origin}/#/claim/${encoded}`; +} + +export function decodeTransferLink(encoded: string): TransferLink { + const base64 = encoded.replace(/-/g, '+').replace(/_/g, '/'); + const json = atob(base64); + return JSON.parse(json) as TransferLink; +} + +export function extractClaimPayload(): TransferLink | null { + const hash = window.location.hash; + const prefix = '#/claim/'; + if (!hash.startsWith(prefix)) { + return null; + } + try { + return decodeTransferLink(hash.slice(prefix.length)); + } catch { + return null; + } +} + +export function isClaimRoute(): boolean { + return window.location.hash.startsWith('#/claim/'); +} From a370b82e80f8b67718fddd52d3c4ea981e2700e9 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Thu, 9 Apr 2026 12:22:54 +0000 Subject: [PATCH 04/24] feat: add sent history service for tracking offchain transfers --- src/services/sentHistoryService.ts | 49 ++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/services/sentHistoryService.ts diff --git a/src/services/sentHistoryService.ts b/src/services/sentHistoryService.ts new file mode 100644 index 0000000..9f33d42 --- /dev/null +++ b/src/services/sentHistoryService.ts @@ -0,0 +1,49 @@ +/** + * Sent History Service + * localStorage CRUD for tracking sent offchain transfers + */ + +export type SentTransferStatus = 'pending' | 'confirmed' | 'expired'; + +export interface SentTransfer { + id: string; + token: 'gc' | 'gcp'; + amount: string; + recipient: string; + link: string; + createdAt: number; + status: SentTransferStatus; +} + +function storageKey(senderAddress: string): string { + return `gregoswap_sent_transfers_${senderAddress}`; +} + +export function getSentTransfers(senderAddress: string): SentTransfer[] { + try { + const raw = localStorage.getItem(storageKey(senderAddress)); + if (!raw) return []; + return JSON.parse(raw) as SentTransfer[]; + } catch { + return []; + } +} + +export function addSentTransfer(senderAddress: string, transfer: SentTransfer): void { + const existing = getSentTransfers(senderAddress); + existing.unshift(transfer); + localStorage.setItem(storageKey(senderAddress), JSON.stringify(existing)); +} + +export function updateSentTransferStatus( + senderAddress: string, + transferId: string, + status: SentTransferStatus, +): void { + const transfers = getSentTransfers(senderAddress); + const index = transfers.findIndex(t => t.id === transferId); + if (index !== -1) { + transfers[index].status = status; + localStorage.setItem(storageKey(senderAddress), JSON.stringify(transfers)); + } +} From b123a59c8460dbb22cbf2f56a7b3e33bfd2351b4 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Thu, 9 Apr 2026 12:24:19 +0000 Subject: [PATCH 05/24] chore: add qrcode.react dependency for transfer link QR codes --- package.json | 1 + yarn.lock | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/package.json b/package.json index 126afe7..3d1e683 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@mui/material": "^6.3.1", "@mui/styles": "^6.3.1", "buffer-json": "^2.0.0", + "qrcode.react": "^4.2.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-dropzone": "^14.3.5", diff --git a/yarn.lock b/yarn.lock index d0decab..c6bc5f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6101,6 +6101,7 @@ __metadata: eslint-plugin-react-refresh: "npm:^0.4.18" globals: "npm:^15.14.0" prettier: "npm:^3.5.3" + qrcode.react: "npm:^4.2.0" react: "npm:^18.3.1" react-dom: "npm:^18.3.1" react-dropzone: "npm:^14.3.5" @@ -8323,6 +8324,15 @@ __metadata: languageName: node linkType: hard +"qrcode.react@npm:^4.2.0": + version: 4.2.0 + resolution: "qrcode.react@npm:4.2.0" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10c0/68c691d130e5fda2f57cee505ed7aea840e7d02033100687b764601f9595e1116e34c13876628a93e1a5c2b85e4efc27d30b2fda72e2050c02f3e1c4e998d248 + languageName: node + linkType: hard + "qs@npm:^6.12.3": version: 6.14.0 resolution: "qs@npm:6.14.0" From c8a02b4191e24d7291ec95feb2fc617284e09273 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Thu, 9 Apr 2026 12:26:45 +0000 Subject: [PATCH 06/24] feat: add Send context with reducer, provider, and state machine Introduces SendProvider/useSend context following the existing swap context pattern, with a state machine covering idle/sending/generating_link/link_ready/error phases and namespaced action creators. Co-Authored-By: Claude Sonnet 4.6 --- src/contexts/send/SendContext.tsx | 54 +++++++++++++++++++++++ src/contexts/send/index.ts | 2 + src/contexts/send/reducer.ts | 71 +++++++++++++++++++++++++++++++ src/main.tsx | 5 ++- 4 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 src/contexts/send/SendContext.tsx create mode 100644 src/contexts/send/index.ts create mode 100644 src/contexts/send/reducer.ts diff --git a/src/contexts/send/SendContext.tsx b/src/contexts/send/SendContext.tsx new file mode 100644 index 0000000..ef612dd --- /dev/null +++ b/src/contexts/send/SendContext.tsx @@ -0,0 +1,54 @@ +/** + * Send Context + * Manages offchain transfer flow and link generation + */ + +import { createContext, useContext, type ReactNode } from 'react'; +import { useSendReducer, type SendState, type SendPhase } from './reducer'; + +interface SendContextType extends SendState { + setToken: (token: 'gc' | 'gcp') => void; + setRecipientAddress: (address: string) => void; + setAmount: (amount: string) => void; + startSend: () => void; + generatingLink: () => void; + linkReady: (link: string) => void; + sendError: (error: string) => void; + dismissError: () => void; + reset: () => void; +} + +const SendContext = createContext(undefined); + +export function useSend() { + const context = useContext(SendContext); + if (context === undefined) { + throw new Error('useSend must be used within a SendProvider'); + } + return context; +} + +interface SendProviderProps { + children: ReactNode; +} + +export function SendProvider({ children }: SendProviderProps) { + const [state, actions] = useSendReducer(); + + const value: SendContextType = { + ...state, + setToken: actions.setToken, + setRecipientAddress: actions.setRecipientAddress, + setAmount: actions.setAmount, + startSend: actions.startSend, + generatingLink: actions.generatingLink, + linkReady: actions.linkReady, + sendError: actions.sendError, + dismissError: actions.dismissError, + reset: actions.reset, + }; + + return {children}; +} + +export type { SendPhase }; diff --git a/src/contexts/send/index.ts b/src/contexts/send/index.ts new file mode 100644 index 0000000..5da65e8 --- /dev/null +++ b/src/contexts/send/index.ts @@ -0,0 +1,2 @@ +export { SendProvider, useSend } from './SendContext'; +export type { SendPhase, SendState } from './reducer'; diff --git a/src/contexts/send/reducer.ts b/src/contexts/send/reducer.ts new file mode 100644 index 0000000..cbb896e --- /dev/null +++ b/src/contexts/send/reducer.ts @@ -0,0 +1,71 @@ +/** + * Send Reducer + * Manages send flow state and transaction phases + */ + +import { createReducerHook, type ActionsFrom } from '../utils'; + +// State +export type SendPhase = 'idle' | 'sending' | 'generating_link' | 'link_ready' | 'error'; + +export interface SendState { + token: 'gc' | 'gcp'; + recipientAddress: string; + amount: string; + phase: SendPhase; + error: string | null; + generatedLink: string | null; +} + +export const initialSendState: SendState = { + token: 'gc', + recipientAddress: '', + amount: '', + phase: 'idle', + error: null, + generatedLink: null, +}; + +// Actions (namespaced with 'send/') +export const sendActions = { + setToken: (token: 'gc' | 'gcp') => ({ type: 'send/SET_TOKEN' as const, token }), + setRecipientAddress: (address: string) => ({ type: 'send/SET_RECIPIENT' as const, address }), + setAmount: (amount: string) => ({ type: 'send/SET_AMOUNT' as const, amount }), + startSend: () => ({ type: 'send/START_SEND' as const }), + generatingLink: () => ({ type: 'send/GENERATING_LINK' as const }), + linkReady: (link: string) => ({ type: 'send/LINK_READY' as const, link }), + sendError: (error: string) => ({ type: 'send/SEND_ERROR' as const, error }), + dismissError: () => ({ type: 'send/DISMISS_ERROR' as const }), + reset: () => ({ type: 'send/RESET' as const }), +}; + +export type SendAction = ActionsFrom; + +// Reducer +export function sendReducer(state: SendState, action: SendAction): SendState { + switch (action.type) { + case 'send/SET_TOKEN': + return { ...state, token: action.token }; + case 'send/SET_RECIPIENT': + return { ...state, recipientAddress: action.address }; + case 'send/SET_AMOUNT': + return { ...state, amount: action.amount }; + case 'send/START_SEND': + return { ...state, phase: 'sending', error: null, generatedLink: null }; + case 'send/GENERATING_LINK': + return { ...state, phase: 'generating_link' }; + case 'send/LINK_READY': + return { ...state, phase: 'link_ready', generatedLink: action.link }; + case 'send/SEND_ERROR': + return { ...state, phase: 'error', error: action.error }; + case 'send/DISMISS_ERROR': + return { ...state, phase: 'idle', error: null }; + case 'send/RESET': + return { ...initialSendState }; + default: + return state; + } +} + +// Hook +export const useSendReducer = createReducerHook(sendReducer, sendActions, initialSendState); diff --git a/src/main.tsx b/src/main.tsx index c8cb4b1..f7e705f 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -5,6 +5,7 @@ import { NetworkProvider } from './contexts/network/NetworkContext'; import { WalletProvider } from './contexts/wallet/WalletContext'; import { ContractsProvider } from './contexts/contracts/ContractsContext'; import { SwapProvider } from './contexts/swap/SwapContext'; +import { SendProvider } from './contexts/send/SendContext'; import { OnboardingProvider } from './contexts/onboarding/OnboardingContext'; createRoot(document.getElementById('root')!).render( @@ -14,7 +15,9 @@ createRoot(document.getElementById('root')!).render( - + + + From 24866c6cefb24bcc130dc1799fd4b5096e54d825 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Thu, 9 Apr 2026 12:28:41 +0000 Subject: [PATCH 07/24] feat: add executeTransferOffchain to contract service Handles offchain transfer execution, sender change note self-delivery, and recipient message extraction for link encoding. --- src/services/contractService.ts | 66 +++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/src/services/contractService.ts b/src/services/contractService.ts index 402e2ee..237e84f 100644 --- a/src/services/contractService.ts +++ b/src/services/contractService.ts @@ -541,6 +541,60 @@ export async function executeDrip( return receipt; } +/** + * Offchain message returned by transfer_offchain + */ +export interface OffchainMessage { + recipient: AztecAddress; + payload: Fr[]; + contractAddress: AztecAddress; + anchorBlockTimestamp: bigint; +} + +/** + * Execute an offchain token transfer. + * Sends tokens privately with offchain note delivery, self-delivers the sender's + * change note, and returns the recipient's offchain messages for link encoding. + */ +export async function executeTransferOffchain( + contracts: SwapContracts, + tokenKey: 'gregoCoin' | 'gregoCoinPremium', + fromAddress: AztecAddress, + recipient: AztecAddress, + amount: bigint, +): Promise<{ receipt: TxReceipt; offchainMessages: OffchainMessage[] }> { + const token = contracts[tokenKey]; + + // 1. Send the offchain transfer transaction + const { receipt, offchainMessages } = await (token.methods as any) + .transfer_offchain(recipient, amount) + .send({ from: fromAddress }); + + // 2. Self-deliver sender's change note (manual until F-324 lands) + const senderMessages = offchainMessages.filter( + (msg: OffchainMessage) => msg.recipient.equals(fromAddress), + ); + if (senderMessages.length > 0) { + await (token.methods as any) + .offchain_receive( + senderMessages.map((msg: OffchainMessage) => ({ + ciphertext: msg.payload, + recipient: fromAddress, + tx_hash: receipt.txHash.hash, + anchor_block_timestamp: msg.anchorBlockTimestamp, + })), + ) + .simulate({ from: fromAddress }); + } + + // 3. Filter and return recipient's messages for link encoding + const recipientMessages = offchainMessages.filter( + (msg: OffchainMessage) => msg.recipient.equals(recipient), + ); + + return { receipt, offchainMessages: recipientMessages }; +} + /** * Parses a drip error into a user-friendly message */ @@ -566,3 +620,15 @@ export function parseDripError(error: unknown): string { return message; } + +/** + * Parses a send (offchain transfer) error into a user-friendly message + */ +export function parseSendError(error: unknown): string { + if (!(error instanceof Error)) return 'Send failed. Please try again.'; + const msg = error.message; + if (msg.includes('Balance too low')) return 'Insufficient token balance'; + if (msg.includes('User denied') || msg.includes('rejected')) return 'Transaction was rejected in wallet'; + if (msg.includes('invalid') && msg.includes('address')) return 'Invalid recipient address'; + return msg; +} From d21e427f87662e18b5a8a6ca8b99ed72d71c36e1 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Thu, 9 Apr 2026 12:29:19 +0000 Subject: [PATCH 08/24] feat: add Send UI components SendForm (token selector, address, amount), SendProgress, LinkDisplay (copyable link + QR code), SentHistory, and SendContainer orchestrator. --- src/components/send/LinkDisplay.tsx | 41 ++++++++++++++++ src/components/send/SendContainer.tsx | 51 ++++++++++++++++++++ src/components/send/SendForm.tsx | 35 ++++++++++++++ src/components/send/SendProgress.tsx | 22 +++++++++ src/components/send/SentHistory.tsx | 68 +++++++++++++++++++++++++++ 5 files changed, 217 insertions(+) create mode 100644 src/components/send/LinkDisplay.tsx create mode 100644 src/components/send/SendContainer.tsx create mode 100644 src/components/send/SendForm.tsx create mode 100644 src/components/send/SendProgress.tsx create mode 100644 src/components/send/SentHistory.tsx diff --git a/src/components/send/LinkDisplay.tsx b/src/components/send/LinkDisplay.tsx new file mode 100644 index 0000000..46a5861 --- /dev/null +++ b/src/components/send/LinkDisplay.tsx @@ -0,0 +1,41 @@ +import { Box, Typography, Button, IconButton, Snackbar } from '@mui/material'; +import ContentCopyIcon from '@mui/icons-material/ContentCopy'; +import { QRCodeSVG } from 'qrcode.react'; +import { useState } from 'react'; + +interface LinkDisplayProps { + link: string; + amount: string; + token: 'gc' | 'gcp'; + recipient: string; + onReset: () => void; +} + +export function LinkDisplay({ link, amount, token, recipient, onReset }: LinkDisplayProps) { + const [copied, setCopied] = useState(false); + const tokenName = token === 'gc' ? 'GregoCoin' : 'GregoCoinPremium'; + + const handleCopy = async () => { + await navigator.clipboard.writeText(link); + setCopied(true); + }; + + return ( + + Sent! + + {amount} {tokenName} → {recipient.slice(0, 8)}...{recipient.slice(-4)} + + + {link} + + + + + + Scan to claim + + setCopied(false)} message="Link copied!" /> + + ); +} diff --git a/src/components/send/SendContainer.tsx b/src/components/send/SendContainer.tsx new file mode 100644 index 0000000..0a09840 --- /dev/null +++ b/src/components/send/SendContainer.tsx @@ -0,0 +1,51 @@ +import { Box, Alert } from '@mui/material'; +import { useSend } from '../../contexts/send'; +import { useWallet } from '../../contexts/wallet'; +import { useContracts } from '../../contexts/contracts'; +import { SendForm } from './SendForm'; +import { SendProgress } from './SendProgress'; +import { LinkDisplay } from './LinkDisplay'; +import { SentHistory } from './SentHistory'; +import { useEffect, useState } from 'react'; + +export function SendContainer() { + const { phase, error, generatedLink, token, amount, recipientAddress, dismissError, reset } = useSend(); + const { currentAddress, isUsingEmbeddedWallet } = useWallet(); + const { fetchBalances } = useContracts(); + const [balances, setBalances] = useState<{ gc: bigint | null; gcp: bigint | null }>({ gc: null, gcp: null }); + + useEffect(() => { + if (currentAddress && !isUsingEmbeddedWallet) { + fetchBalances().then(([gc, gcp]) => setBalances({ gc, gcp })); + } + }, [currentAddress, isUsingEmbeddedWallet, fetchBalances]); + + useEffect(() => { + if (phase === 'link_ready' && currentAddress) { + fetchBalances().then(([gc, gcp]) => setBalances({ gc, gcp })); + } + }, [phase, currentAddress, fetchBalances]); + + if (isUsingEmbeddedWallet) { + return ( + + Connect an external wallet to send tokens. + + ); + } + + return ( + + {phase === 'link_ready' && generatedLink ? ( + + ) : ( + <> + + + + )} + {error && {error}} + {currentAddress && } + + ); +} diff --git a/src/components/send/SendForm.tsx b/src/components/send/SendForm.tsx new file mode 100644 index 0000000..f2eefb6 --- /dev/null +++ b/src/components/send/SendForm.tsx @@ -0,0 +1,35 @@ +import { Box, TextField, Typography, ToggleButton, ToggleButtonGroup, Button } from '@mui/material'; +import { useSend } from '../../contexts/send'; + +interface SendFormProps { + balance: { gc: bigint | null; gcp: bigint | null }; +} + +export function SendForm({ balance }: SendFormProps) { + const { token, recipientAddress, amount, phase, setToken, setRecipientAddress, setAmount } = useSend(); + const isSending = phase === 'sending' || phase === 'generating_link'; + const currentBalance = token === 'gc' ? balance.gc : balance.gcp; + const canSend = !!amount && parseFloat(amount) > 0 && !!recipientAddress && !isSending; + + return ( + + + + Token + + v && setToken(v)} size="small" fullWidth disabled={isSending}> + GregoCoin + GregoCoinPremium + + + setRecipientAddress(e.target.value)} fullWidth disabled={isSending} size="small" /> + + setAmount(e.target.value)} fullWidth disabled={isSending} size="small" + slotProps={{ input: { endAdornment: currentBalance !== null ? Balance: {currentBalance.toString()} : null } }} /> + + + + ); +} diff --git a/src/components/send/SendProgress.tsx b/src/components/send/SendProgress.tsx new file mode 100644 index 0000000..945c4e3 --- /dev/null +++ b/src/components/send/SendProgress.tsx @@ -0,0 +1,22 @@ +import { Box, Typography, CircularProgress } from '@mui/material'; +import type { SendPhase } from '../../contexts/send'; + +interface SendProgressProps { + phase: SendPhase; +} + +const phaseMessages: Record = { + sending: 'Sending transaction...', + generating_link: 'Generating claim link...', +}; + +export function SendProgress({ phase }: SendProgressProps) { + const message = phaseMessages[phase]; + if (!message) return null; + return ( + + + {message} + + ); +} diff --git a/src/components/send/SentHistory.tsx b/src/components/send/SentHistory.tsx new file mode 100644 index 0000000..3ce56bb --- /dev/null +++ b/src/components/send/SentHistory.tsx @@ -0,0 +1,68 @@ +import { Box, Typography, IconButton, Snackbar, Chip } from '@mui/material'; +import ContentCopyIcon from '@mui/icons-material/ContentCopy'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import { useState } from 'react'; +import { getSentTransfers, type SentTransfer } from '../../services/sentHistoryService'; + +interface SentHistoryProps { + senderAddress: string; +} + +function timeAgo(timestamp: number): string { + const seconds = Math.floor((Date.now() - timestamp) / 1000); + if (seconds < 60) return `${seconds}s ago`; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + return `${Math.floor(hours / 24)}d ago`; +} + +function StatusChip({ status }: { status: SentTransfer['status'] }) { + if (status === 'confirmed') return null; + const color = status === 'pending' ? 'warning' : 'error'; + return ; +} + +export function SentHistory({ senderAddress }: SentHistoryProps) { + const [copied, setCopied] = useState(false); + const [expanded, setExpanded] = useState(false); + const transfers = getSentTransfers(senderAddress); + + if (transfers.length === 0) return null; + + const visibleTransfers = expanded ? transfers : transfers.slice(0, 3); + const hasMore = transfers.length > 3; + + const handleCopy = async (link: string) => { + await navigator.clipboard.writeText(link); + setCopied(true); + }; + + return ( + + Sent transfers + {visibleTransfers.map(transfer => ( + + + {transfer.amount} {transfer.token === 'gc' ? 'GC' : 'GCP'} + → {transfer.recipient.slice(0, 8)}...{transfer.recipient.slice(-4)} + + + + {timeAgo(transfer.createdAt)} + handleCopy(transfer.link)}> + + + ))} + {hasMore && ( + + setExpanded(!expanded)} sx={{ transform: expanded ? 'rotate(180deg)' : 'none', transition: '0.2s' }}> + + + + )} + setCopied(false)} message="Link copied!" /> + + ); +} From dec1846039b3f77d08c68a19a2dca008aad89513 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Thu, 9 Apr 2026 12:29:20 +0000 Subject: [PATCH 09/24] feat: add Claim page components ClaimPage (orchestrator with state machine), ClaimProgress, and ClaimSuccess. Claim contract interaction to be wired in Task 10. --- src/components/claim/ClaimPage.tsx | 87 ++++++++++++++++++++++++++ src/components/claim/ClaimProgress.tsx | 21 +++++++ src/components/claim/ClaimSuccess.tsx | 23 +++++++ 3 files changed, 131 insertions(+) create mode 100644 src/components/claim/ClaimPage.tsx create mode 100644 src/components/claim/ClaimProgress.tsx create mode 100644 src/components/claim/ClaimSuccess.tsx diff --git a/src/components/claim/ClaimPage.tsx b/src/components/claim/ClaimPage.tsx new file mode 100644 index 0000000..eb8ba62 --- /dev/null +++ b/src/components/claim/ClaimPage.tsx @@ -0,0 +1,87 @@ +import { Box, Typography, Button, Alert, CircularProgress, Container, Chip } from '@mui/material'; +import { useEffect, useState, useCallback } from 'react'; +import { extractClaimPayload, type TransferLink } from '../../services/offchainLinkService'; +import { ClaimProgress } from './ClaimProgress'; +import { ClaimSuccess } from './ClaimSuccess'; +import { GregoSwapLogo } from '../GregoSwapLogo'; + +type ClaimState = + | { phase: 'decoding' } + | { phase: 'preview'; data: TransferLink } + | { phase: 'claiming'; data: TransferLink } + | { phase: 'verifying'; data: TransferLink } + | { phase: 'claimed'; data: TransferLink; verified: boolean } + | { phase: 'error'; message: string }; + +export function ClaimPage() { + const [state, setState] = useState({ phase: 'decoding' }); + + // Step 1: Decode the link on mount + useEffect(() => { + const data = extractClaimPayload(); + if (!data) { + setState({ phase: 'error', message: 'Invalid or missing claim link.' }); + return; + } + setState({ phase: 'preview', data }); + }, []); + + // Step 2: Execute the claim (will be fully wired in Task 10) + const doClaim = useCallback(async () => { + if (state.phase !== 'preview') return; + const { data } = state; + setState({ phase: 'claiming', data }); + + try { + // TODO(Task 10): Wire offchain_receive via ContractsContext + // 1. Ensure wallet is connected/created + // 2. Register token contract + // 3. Call claimOffchainTransfer(tokenKey, message) + // 4. Verify balance + + // For now, simulate the flow progression + setState({ phase: 'error', message: 'Claim not yet wired — will be connected in Task 10.' }); + } catch (error) { + const message = error instanceof Error ? error.message : 'Claim failed. Please try again.'; + setState({ phase: 'error', message }); + } + }, [state]); + + const handleGoToSwap = () => { + window.location.hash = ''; + window.location.reload(); + }; + + const tokenName = (t: string) => (t === 'gc' ? 'GregoCoin' : 'GregoCoinPremium'); + + return ( + + + + + + {state.phase === 'decoding' && ( + + )} + {state.phase === 'preview' && ( + + Someone sent you + + + {state.data.amount} {tokenName(state.data.token)} + + + + + + )} + {state.phase === 'claiming' && } + {state.phase === 'verifying' && } + {state.phase === 'claimed' && ( + + )} + {state.phase === 'error' && {state.message}} + + + ); +} diff --git a/src/components/claim/ClaimProgress.tsx b/src/components/claim/ClaimProgress.tsx new file mode 100644 index 0000000..fb9baf5 --- /dev/null +++ b/src/components/claim/ClaimProgress.tsx @@ -0,0 +1,21 @@ +import { Box, Typography, CircularProgress } from '@mui/material'; + +type ClaimPhase = 'claiming' | 'verifying'; + +interface ClaimProgressProps { + phase: ClaimPhase; +} + +const phaseMessages: Record = { + claiming: 'Claiming tokens...', + verifying: 'Verifying amount...', +}; + +export function ClaimProgress({ phase }: ClaimProgressProps) { + return ( + + + {phaseMessages[phase]} + + ); +} diff --git a/src/components/claim/ClaimSuccess.tsx b/src/components/claim/ClaimSuccess.tsx new file mode 100644 index 0000000..b66efbf --- /dev/null +++ b/src/components/claim/ClaimSuccess.tsx @@ -0,0 +1,23 @@ +import { Box, Typography, Button, Chip } from '@mui/material'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; + +interface ClaimSuccessProps { + amount: string; + tokenName: string; + verified: boolean; + onGoToSwap: () => void; +} + +export function ClaimSuccess({ amount, tokenName, verified, onGoToSwap }: ClaimSuccessProps) { + return ( + + + Tokens Claimed! + + {amount} {tokenName} + + + + + ); +} From eac37bb00a34e24935f929fac315e17f228b6fa0 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Thu, 9 Apr 2026 12:31:26 +0000 Subject: [PATCH 10/24] feat: add hash routing for claim page and Swap/Send tab bar Detects /#/claim/ routes and renders ClaimPage. Adds Swap/Send tabs to the main interface. Co-Authored-By: Claude Sonnet 4.6 --- src/App.tsx | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 3 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index e50ca9d..88054b6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,10 +1,14 @@ -import { ThemeProvider, CssBaseline, Container, Box, Typography } from '@mui/material'; +import { useState } from 'react'; +import { ThemeProvider, CssBaseline, Container, Box, Typography, Tabs, Tab } from '@mui/material'; import { theme } from './theme'; import { GregoSwapLogo } from './components/GregoSwapLogo'; import { WalletChip } from './components/WalletChip'; import { NetworkSwitcher } from './components/NetworkSwitcher'; import { FooterInfo } from './components/FooterInfo'; import { SwapContainer } from './components/swap'; +import { SendContainer } from './components/send/SendContainer'; +import { ClaimPage } from './components/claim/ClaimPage'; +import { isClaimRoute } from './services/offchainLinkService'; import { useWallet } from './contexts/wallet'; import { useOnboarding } from './contexts/onboarding'; import { OnboardingModal } from './components/OnboardingModal'; @@ -12,6 +16,7 @@ import { TxNotificationCenter } from './components/TxNotificationCenter'; import type { AztecAddress } from '@aztec/aztec.js/addresses'; export function App() { + const [activeTab, setActiveTab] = useState(0); const { disconnectWallet, setCurrentAddress, currentAddress, error: walletError, isLoading: walletLoading } = useWallet(); const { isOnboardingModalOpen, startOnboarding, resetOnboarding, status: onboardingStatus } = useOnboarding(); @@ -30,6 +35,40 @@ export function App() { resetOnboarding(); }; + if (isClaimRoute()) { + return ( + + + + + + + ); + } + return ( @@ -79,8 +118,25 @@ export function App() { - {/* Swap Interface */} - + {/* Tab Bar */} + setActiveTab(value)} + centered + sx={{ + mb: 3, + '& .MuiTab-root': { color: 'text.secondary', fontWeight: 600 }, + '& .Mui-selected': { color: 'primary.main' }, + '& .MuiTabs-indicator': { backgroundColor: 'primary.main' }, + }} + > + + + + + {/* Tab Content */} + {activeTab === 0 && } + {activeTab === 1 && } {/* Wallet Error Display */} {walletError && ( From 6f2a2666ce53395c625cb09fcd16ba5a744212d4 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Thu, 9 Apr 2026 12:34:28 +0000 Subject: [PATCH 11/24] feat: wire offchain transfer methods through ContractsContext Add sendOffchain and claimOffchainTransfer to ContractsContext. Update SendContext with executeSend business logic. Wire SendForm button and ClaimPage contract interaction. --- src/components/claim/ClaimPage.tsx | 56 +++++++++++++--- src/components/send/SendForm.tsx | 5 +- src/contexts/contracts/ContractsContext.tsx | 40 +++++++++++ src/contexts/send/SendContext.tsx | 74 ++++++++++++++++++++- 4 files changed, 161 insertions(+), 14 deletions(-) diff --git a/src/components/claim/ClaimPage.tsx b/src/components/claim/ClaimPage.tsx index eb8ba62..31e6bb7 100644 --- a/src/components/claim/ClaimPage.tsx +++ b/src/components/claim/ClaimPage.tsx @@ -4,6 +4,8 @@ import { extractClaimPayload, type TransferLink } from '../../services/offchainL import { ClaimProgress } from './ClaimProgress'; import { ClaimSuccess } from './ClaimSuccess'; import { GregoSwapLogo } from '../GregoSwapLogo'; +import { useContracts } from '../../contexts/contracts'; +import { useWallet } from '../../contexts/wallet'; type ClaimState = | { phase: 'decoding' } @@ -15,6 +17,8 @@ type ClaimState = export function ClaimPage() { const [state, setState] = useState({ phase: 'decoding' }); + const { claimOffchainTransfer, registerBaseContracts, fetchBalances, isLoadingContracts } = useContracts(); + const { wallet, currentAddress } = useWallet(); // Step 1: Decode the link on mount useEffect(() => { @@ -26,26 +30,58 @@ export function ClaimPage() { setState({ phase: 'preview', data }); }, []); - // Step 2: Execute the claim (will be fully wired in Task 10) + // Step 2: Execute the claim const doClaim = useCallback(async () => { if (state.phase !== 'preview') return; const { data } = state; setState({ phase: 'claiming', data }); try { - // TODO(Task 10): Wire offchain_receive via ContractsContext - // 1. Ensure wallet is connected/created - // 2. Register token contract - // 3. Call claimOffchainTransfer(tokenKey, message) - // 4. Verify balance - - // For now, simulate the flow progression - setState({ phase: 'error', message: 'Claim not yet wired — will be connected in Task 10.' }); + // Ensure contracts are registered + if (wallet && !isLoadingContracts) { + try { await registerBaseContracts(); } catch { /* may already be registered */ } + } + + if (!wallet || !currentAddress) { + setState({ phase: 'error', message: 'No wallet available. Please refresh and try again.' }); + return; + } + + // Get balance before claim (for verification) + let balanceBefore = 0n; + try { + const [gc, gcp] = await fetchBalances(); + balanceBefore = data.token === 'gc' ? gc : gcp; + } catch { /* new wallet may have no balance */ } + + // Reconstruct Fr values and call offchain_receive + const { Fr } = await import('@aztec/aztec.js/fields'); + const { AztecAddress } = await import('@aztec/aztec.js/addresses'); + + const tokenKey = data.token === 'gc' ? 'gregoCoin' as const : 'gregoCoinPremium' as const; + + await claimOffchainTransfer(tokenKey, { + ciphertext: data.payload.map((s: string) => Fr.fromString(s)), + recipient: AztecAddress.fromString(data.recipient), + tx_hash: data.txHash, + anchor_block_timestamp: BigInt(data.anchorBlockTimestamp), + }); + + setState({ phase: 'verifying', data }); + + // Verify balance + const [gcAfter, gcpAfter] = await fetchBalances(); + const balanceAfter = data.token === 'gc' ? gcAfter : gcpAfter; + const received = balanceAfter - balanceBefore; + const expectedAmount = BigInt(Math.round(parseFloat(data.amount))); + const verified = received >= expectedAmount; + + setState({ phase: 'claimed', data, verified }); } catch (error) { const message = error instanceof Error ? error.message : 'Claim failed. Please try again.'; setState({ phase: 'error', message }); } - }, [state]); + }, [state, wallet, currentAddress, isLoadingContracts, registerBaseContracts, fetchBalances, claimOffchainTransfer]); const handleGoToSwap = () => { window.location.hash = ''; diff --git a/src/components/send/SendForm.tsx b/src/components/send/SendForm.tsx index f2eefb6..e091fc6 100644 --- a/src/components/send/SendForm.tsx +++ b/src/components/send/SendForm.tsx @@ -6,10 +6,9 @@ interface SendFormProps { } export function SendForm({ balance }: SendFormProps) { - const { token, recipientAddress, amount, phase, setToken, setRecipientAddress, setAmount } = useSend(); + const { token, recipientAddress, amount, phase, setToken, setRecipientAddress, setAmount, canSend, executeSend } = useSend(); const isSending = phase === 'sending' || phase === 'generating_link'; const currentBalance = token === 'gc' ? balance.gc : balance.gcp; - const canSend = !!amount && parseFloat(amount) > 0 && !!recipientAddress && !isSending; return ( @@ -27,7 +26,7 @@ export function SendForm({ balance }: SendFormProps) { setAmount(e.target.value)} fullWidth disabled={isSending} size="small" slotProps={{ input: { endAdornment: currentBalance !== null ? Balance: {currentBalance.toString()} : null } }} /> - diff --git a/src/contexts/contracts/ContractsContext.tsx b/src/contexts/contracts/ContractsContext.tsx index d7ae7a6..2eee315 100644 --- a/src/contexts/contracts/ContractsContext.tsx +++ b/src/contexts/contracts/ContractsContext.tsx @@ -27,6 +27,8 @@ interface ContractsContextType { fetchBalances: () => Promise<[bigint, bigint]>; simulateOnboardingQueries: () => Promise<[number, bigint, bigint]>; drip: (password: string, recipient: AztecAddress) => Promise; + sendOffchain: (tokenKey: 'gregoCoin' | 'gregoCoinPremium', recipient: AztecAddress, amount: bigint) => Promise<{ receipt: TxReceipt; offchainMessages: any[] }>; + claimOffchainTransfer: (tokenKey: 'gregoCoin' | 'gregoCoinPremium', message: { ciphertext: any[]; recipient: AztecAddress; tx_hash: string; anchor_block_timestamp: bigint }) => Promise; } const ContractsContext = createContext(undefined); @@ -219,6 +221,42 @@ export function ContractsProvider({ children }: ContractsProviderProps) { [wallet, activeNetwork, state.contracts.pop], ); + // Execute offchain transfer (send with link) + const sendOffchain = useCallback(async ( + tokenKey: 'gregoCoin' | 'gregoCoinPremium', + recipient: AztecAddress, + amount: bigint, + ) => { + if (!wallet || !currentAddress || !state.contracts.gregoCoin || !state.contracts.gregoCoinPremium || !state.contracts.amm) { + throw new Error('Contracts not initialized'); + } + return contractService.executeTransferOffchain( + { + gregoCoin: state.contracts.gregoCoin, + gregoCoinPremium: state.contracts.gregoCoinPremium, + amm: state.contracts.amm, + }, + tokenKey, + currentAddress, + recipient, + amount, + ); + }, [wallet, currentAddress, state.contracts]); + + // Claim an offchain transfer via offchain_receive + const claimOffchainTransfer = useCallback(async ( + tokenKey: 'gregoCoin' | 'gregoCoinPremium', + message: { ciphertext: any[]; recipient: AztecAddress; tx_hash: string; anchor_block_timestamp: bigint }, + ) => { + if (!wallet || !currentAddress || !state.contracts.gregoCoin || !state.contracts.gregoCoinPremium) { + throw new Error('Contracts not initialized'); + } + const token = tokenKey === 'gregoCoin' ? state.contracts.gregoCoin : state.contracts.gregoCoinPremium; + await (token.methods as any) + .offchain_receive([message]) + .simulate({ from: currentAddress }); + }, [wallet, currentAddress, state.contracts]); + // Initialize contracts for embedded wallet useEffect(() => { async function initializeContracts() { @@ -253,6 +291,8 @@ export function ContractsProvider({ children }: ContractsProviderProps) { fetchBalances, simulateOnboardingQueries, drip, + sendOffchain, + claimOffchainTransfer, }; return {children}; diff --git a/src/contexts/send/SendContext.tsx b/src/contexts/send/SendContext.tsx index ef612dd..2b68bf8 100644 --- a/src/contexts/send/SendContext.tsx +++ b/src/contexts/send/SendContext.tsx @@ -3,8 +3,14 @@ * Manages offchain transfer flow and link generation */ -import { createContext, useContext, type ReactNode } from 'react'; +import { createContext, useContext, type ReactNode, useCallback } from 'react'; +import { AztecAddress } from '@aztec/aztec.js/addresses'; import { useSendReducer, type SendState, type SendPhase } from './reducer'; +import { useContracts } from '../contracts'; +import { useWallet } from '../wallet'; +import { useNetwork } from '../network'; +import { encodeTransferLink, type TransferLink } from '../../services/offchainLinkService'; +import { addSentTransfer } from '../../services/sentHistoryService'; interface SendContextType extends SendState { setToken: (token: 'gc' | 'gcp') => void; @@ -16,6 +22,8 @@ interface SendContextType extends SendState { sendError: (error: string) => void; dismissError: () => void; reset: () => void; + canSend: boolean; + executeSend: () => Promise; } const SendContext = createContext(undefined); @@ -34,6 +42,68 @@ interface SendProviderProps { export function SendProvider({ children }: SendProviderProps) { const [state, actions] = useSendReducer(); + const { sendOffchain, isLoadingContracts } = useContracts(); + const { currentAddress, isUsingEmbeddedWallet } = useWallet(); + const { activeNetwork } = useNetwork(); + + const canSend = + !!state.amount && + parseFloat(state.amount) > 0 && + !!state.recipientAddress && + !isLoadingContracts && + !isUsingEmbeddedWallet && + !!currentAddress; + + const executeSend = useCallback(async () => { + if (!currentAddress || !state.recipientAddress || !state.amount) { + actions.sendError('Missing required fields'); + return; + } + + actions.startSend(); + + try { + const recipient = AztecAddress.fromString(state.recipientAddress); + const amount = BigInt(Math.round(parseFloat(state.amount))); + const tokenKey = state.token === 'gc' ? 'gregoCoin' as const : 'gregoCoinPremium' as const; + const contractAddress = activeNetwork.contracts[tokenKey]; + + const { receipt, offchainMessages } = await sendOffchain(tokenKey, recipient, amount); + + actions.generatingLink(); + + const recipientMessage = offchainMessages[0]; + if (!recipientMessage) { + throw new Error('No offchain message generated for recipient'); + } + + const linkData: TransferLink = { + token: state.token, + amount: state.amount, + recipient: state.recipientAddress, + contractAddress, + txHash: receipt.txHash.toString(), + anchorBlockTimestamp: recipientMessage.anchorBlockTimestamp.toString(), + payload: recipientMessage.payload.map((f: any) => f.toString()), + }; + + const link = encodeTransferLink(linkData); + actions.linkReady(link); + + addSentTransfer(currentAddress.toString(), { + id: receipt.txHash.toString(), + token: state.token, + amount: state.amount, + recipient: state.recipientAddress, + link, + createdAt: Date.now(), + status: 'confirmed', + }); + } catch (error) { + const message = error instanceof Error ? error.message : 'Send failed. Please try again.'; + actions.sendError(message); + } + }, [currentAddress, state.recipientAddress, state.amount, state.token, activeNetwork, sendOffchain, actions]); const value: SendContextType = { ...state, @@ -46,6 +116,8 @@ export function SendProvider({ children }: SendProviderProps) { sendError: actions.sendError, dismissError: actions.dismissError, reset: actions.reset, + canSend, + executeSend, }; return {children}; From c4392f93aeb0dda87b81b94f52162feab4d88da9 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Thu, 9 Apr 2026 12:54:07 +0000 Subject: [PATCH 12/24] chore: add Token to workspace for compilation --- contracts/Nargo.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/contracts/Nargo.toml b/contracts/Nargo.toml index dcda87a..c429506 100644 --- a/contracts/Nargo.toml +++ b/contracts/Nargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ "proof_of_password", - "amm" + "amm", + "token" ] \ No newline at end of file From a0fb70adad9eb7f0cabd12c1652b828162318c8d Mon Sep 17 00:00:00 2001 From: mverzilli Date: Thu, 9 Apr 2026 15:14:48 +0000 Subject: [PATCH 13/24] chore: add deploy-subscription-fpc script and graceful FPC handling - Add scripts/deploy-subscription-fpc.ts for deploying SubscriptionFPC to local - Make registerDripContracts skip gracefully when SubscriptionFPC is not on-chain - Add design spec and implementation plan docs Co-Authored-By: Claude Opus 4.6 (1M context) --- .../2026-04-09-p2p-offchain-transfers.md | 1793 +++++++++++++++++ ...026-04-09-p2p-offchain-transfers-design.md | 373 ++++ scripts/deploy-subscription-fpc.ts | 24 +- src/services/contractService.ts | 32 +- 4 files changed, 2186 insertions(+), 36 deletions(-) create mode 100644 docs/superpowers/plans/2026-04-09-p2p-offchain-transfers.md create mode 100644 docs/superpowers/specs/2026-04-09-p2p-offchain-transfers-design.md diff --git a/docs/superpowers/plans/2026-04-09-p2p-offchain-transfers.md b/docs/superpowers/plans/2026-04-09-p2p-offchain-transfers.md new file mode 100644 index 0000000..0dd3d86 --- /dev/null +++ b/docs/superpowers/plans/2026-04-09-p2p-offchain-transfers.md @@ -0,0 +1,1793 @@ +# P2P Offchain Transfers Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add P2P private token transfers to GregoSwap using Aztec's offchain delivery, with shareable claim links and QR codes. + +**Architecture:** Fork the standard Token contract to add a `transfer_offchain` method that delivers notes via `MessageDelivery.OFFCHAIN`. The frontend extracts offchain messages from the transaction, encodes them into URLs, and provides a claim page where recipients open the link and call `offchain_receive()` to claim tokens. + +**Tech Stack:** Noir (Aztec contracts), React 18, TypeScript, MUI, Vite, qrcode.react + +**Spec:** `docs/superpowers/specs/2026-04-09-p2p-offchain-transfers-design.md` + +--- + +## File Structure + +``` +contracts/ + token/ # NEW — fork of standard Token contract + src/main.nr # standard Token + transfer_offchain + Nargo.toml # local deps pointing to aztec-packages + amm/Nargo.toml # MOD — token dep → local fork + proof_of_password/Nargo.toml # MOD — token dep → local fork + +src/ + services/ + offchainLinkService.ts # NEW — encode/decode transfer links + sentHistoryService.ts # NEW — localStorage CRUD for sent transfers + contractService.ts # MOD — add executeTransferOffchain + parseSendError + contexts/ + send/ + reducer.ts # NEW — send state machine + SendContext.tsx # NEW — send flow orchestration + index.ts # NEW — exports + components/ + App.tsx # MOD — hash route detection, tab bar + send/ + SendContainer.tsx # NEW — send flow orchestrator + SendForm.tsx # NEW — token selector, address, amount + SendProgress.tsx # NEW — sending state indicator + LinkDisplay.tsx # NEW — copyable link + QR code + SentHistory.tsx # NEW — list of past transfers + claim/ + ClaimPage.tsx # NEW — claim flow orchestrator + ClaimProgress.tsx # NEW — claiming state indicator + ClaimSuccess.tsx # NEW — success state + CTA + main.tsx # MOD — add SendProvider +``` + +--- + +### Task 1: Fork Token Contract + +**Files:** +- Create: `contracts/token/Nargo.toml` +- Create: `contracts/token/src/main.nr` +- Modify: `contracts/amm/Nargo.toml` +- Modify: `contracts/proof_of_password/Nargo.toml` + +- [ ] **Step 1: Create Nargo.toml for the forked token** + +Create `contracts/token/Nargo.toml`: + +```toml +[package] +name = "token_contract" +authors = [""] +type = "contract" + +[dependencies] +aztec = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-aztecnr-rc.2", directory = "noir-projects/aztec-nr/aztec" } +uint_note = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-aztecnr-rc.2", directory = "noir-projects/aztec-nr/uint-note" } +compressed_string = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-aztecnr-rc.2", directory = "noir-projects/aztec-nr/compressed-string" } +balance_set = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-aztecnr-rc.2", directory = "noir-projects/aztec-nr/balance-set" } +``` + +- [ ] **Step 2: Copy the standard Token contract source** + +Copy the Token contract main.nr from the Aztec monorepo: + +```bash +cp /mnt/user-data/martin/aztec-packages/noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr contracts/token/src/main.nr +``` + +Do NOT copy the test files — we don't need them for the fork. + +- [ ] **Step 3: Add the `transfer_offchain` method** + +In `contracts/token/src/main.nr`, add the following method directly after the existing `transfer` function (after line ~254 in the original). The method is identical to `transfer` but uses `MessageDelivery.OFFCHAIN` for all three deliveries: + +```noir + #[external("private")] + fn transfer_offchain(to: AztecAddress, amount: u128) { + let from = self.msg_sender(); + + let change = self.internal.subtract_balance(from, amount, INITIAL_TRANSFER_CALL_MAX_NOTES); + self.storage.balances.at(from).add(change).deliver(MessageDelivery.OFFCHAIN); + self.storage.balances.at(to).add(amount).deliver(MessageDelivery.OFFCHAIN); + + self.emit(Transfer { from, to, amount }).deliver_to( + to, + MessageDelivery.OFFCHAIN, + ); + } +``` + +Note: `MessageDelivery` is already imported at line 27 of the standard Token contract (`messages::message_delivery::MessageDelivery`). No new imports needed. + +- [ ] **Step 4: Remove the test module reference** + +The copied `main.nr` starts with `mod test;` on line 3. Remove this line since we didn't copy the test files. Alternatively, create an empty `contracts/token/src/test.nr` with just `// Tests omitted in fork`. + +- [ ] **Step 5: Update AMM Nargo.toml to use local fork** + +In `contracts/amm/Nargo.toml`, change the token dependency from: + +```toml +token = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-aztecnr-rc.2", directory = "noir-projects/noir-contracts/contracts/app/token_contract" } +``` + +to: + +```toml +token = { path = "../token" } +``` + +- [ ] **Step 6: Update PoP Nargo.toml to use local fork** + +In `contracts/proof_of_password/Nargo.toml`, change the token dependency from: + +```toml +token = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-aztecnr-rc.2", directory = "noir-projects/noir-contracts/contracts/app/token_contract" } +``` + +to: + +```toml +token = { path = "../token" } +``` + +- [ ] **Step 7: Compile contracts** + +```bash +yarn compile:contracts +``` + +Expected: All three contracts compile successfully. The AMM and PoP contracts should work identically since the fork is additive-only. + +- [ ] **Step 8: Commit** + +```bash +git add contracts/token/ contracts/amm/Nargo.toml contracts/proof_of_password/Nargo.toml +git commit -m "feat: fork Token contract with transfer_offchain method + +Add local fork of standard Token contract that adds a transfer_offchain +method using MessageDelivery.OFFCHAIN for all note and event deliveries. +Update AMM and PoP contracts to use local fork." +``` + +--- + +### Task 2: Offchain Link Service + +**Files:** +- Create: `src/services/offchainLinkService.ts` + +- [ ] **Step 1: Create the link service** + +Create `src/services/offchainLinkService.ts`: + +```typescript +/** + * Offchain Link Service + * Encodes/decodes offchain transfer messages into shareable URLs + */ + +export interface TransferLink { + token: 'gc' | 'gcp'; + amount: string; + recipient: string; + contractAddress: string; + txHash: string; + anchorBlockTimestamp: string; + payload: string[]; +} + +export function encodeTransferLink(data: TransferLink): string { + const json = JSON.stringify(data); + const encoded = btoa(json) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); + return `${window.location.origin}/#/claim/${encoded}`; +} + +export function decodeTransferLink(encoded: string): TransferLink { + const base64 = encoded.replace(/-/g, '+').replace(/_/g, '/'); + const json = atob(base64); + return JSON.parse(json) as TransferLink; +} + +export function extractClaimPayload(): TransferLink | null { + const hash = window.location.hash; + const prefix = '#/claim/'; + if (!hash.startsWith(prefix)) { + return null; + } + try { + return decodeTransferLink(hash.slice(prefix.length)); + } catch { + return null; + } +} + +export function isClaimRoute(): boolean { + return window.location.hash.startsWith('#/claim/'); +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add src/services/offchainLinkService.ts +git commit -m "feat: add offchain link service for encoding/decoding transfer URLs" +``` + +--- + +### Task 3: Sent History Service + +**Files:** +- Create: `src/services/sentHistoryService.ts` + +- [ ] **Step 1: Create the sent history service** + +Create `src/services/sentHistoryService.ts`: + +```typescript +/** + * Sent History Service + * localStorage CRUD for tracking sent offchain transfers + */ + +export type SentTransferStatus = 'pending' | 'confirmed' | 'expired'; + +export interface SentTransfer { + id: string; + token: 'gc' | 'gcp'; + amount: string; + recipient: string; + link: string; + createdAt: number; + status: SentTransferStatus; +} + +function storageKey(senderAddress: string): string { + return `gregoswap_sent_transfers_${senderAddress}`; +} + +export function getSentTransfers(senderAddress: string): SentTransfer[] { + try { + const raw = localStorage.getItem(storageKey(senderAddress)); + if (!raw) return []; + return JSON.parse(raw) as SentTransfer[]; + } catch { + return []; + } +} + +export function addSentTransfer(senderAddress: string, transfer: SentTransfer): void { + const existing = getSentTransfers(senderAddress); + existing.unshift(transfer); + localStorage.setItem(storageKey(senderAddress), JSON.stringify(existing)); +} + +export function updateSentTransferStatus( + senderAddress: string, + transferId: string, + status: SentTransferStatus, +): void { + const transfers = getSentTransfers(senderAddress); + const index = transfers.findIndex(t => t.id === transferId); + if (index !== -1) { + transfers[index].status = status; + localStorage.setItem(storageKey(senderAddress), JSON.stringify(transfers)); + } +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add src/services/sentHistoryService.ts +git commit -m "feat: add sent history service for tracking offchain transfers" +``` + +--- + +### Task 4: Send Reducer and Context + +**Files:** +- Create: `src/contexts/send/reducer.ts` +- Create: `src/contexts/send/SendContext.tsx` +- Create: `src/contexts/send/index.ts` +- Modify: `src/main.tsx` + +- [ ] **Step 1: Create the send reducer** + +Create `src/contexts/send/reducer.ts` following the existing pattern from `src/contexts/swap/reducer.ts`: + +```typescript +/** + * Send Reducer + * Manages send flow state and transaction phases + */ + +import { createReducerHook, type ActionsFrom } from '../utils'; + +// ============================================================================= +// State +// ============================================================================= + +export type SendPhase = 'idle' | 'sending' | 'generating_link' | 'link_ready' | 'error'; + +export interface SendState { + token: 'gc' | 'gcp'; + recipientAddress: string; + amount: string; + phase: SendPhase; + error: string | null; + generatedLink: string | null; +} + +export const initialSendState: SendState = { + token: 'gc', + recipientAddress: '', + amount: '', + phase: 'idle', + error: null, + generatedLink: null, +}; + +// ============================================================================= +// Actions +// ============================================================================= + +export const sendActions = { + setToken: (token: 'gc' | 'gcp') => ({ type: 'send/SET_TOKEN' as const, token }), + setRecipientAddress: (address: string) => ({ type: 'send/SET_RECIPIENT' as const, address }), + setAmount: (amount: string) => ({ type: 'send/SET_AMOUNT' as const, amount }), + startSend: () => ({ type: 'send/START_SEND' as const }), + generatingLink: () => ({ type: 'send/GENERATING_LINK' as const }), + linkReady: (link: string) => ({ type: 'send/LINK_READY' as const, link }), + sendError: (error: string) => ({ type: 'send/SEND_ERROR' as const, error }), + dismissError: () => ({ type: 'send/DISMISS_ERROR' as const }), + reset: () => ({ type: 'send/RESET' as const }), +}; + +export type SendAction = ActionsFrom; + +// ============================================================================= +// Reducer +// ============================================================================= + +export function sendReducer(state: SendState, action: SendAction): SendState { + switch (action.type) { + case 'send/SET_TOKEN': + return { ...state, token: action.token }; + + case 'send/SET_RECIPIENT': + return { ...state, recipientAddress: action.address }; + + case 'send/SET_AMOUNT': + return { ...state, amount: action.amount }; + + case 'send/START_SEND': + return { ...state, phase: 'sending', error: null, generatedLink: null }; + + case 'send/GENERATING_LINK': + return { ...state, phase: 'generating_link' }; + + case 'send/LINK_READY': + return { ...state, phase: 'link_ready', generatedLink: action.link }; + + case 'send/SEND_ERROR': + return { ...state, phase: 'error', error: action.error }; + + case 'send/DISMISS_ERROR': + return { ...state, phase: 'idle', error: null }; + + case 'send/RESET': + return { ...initialSendState }; + + default: + return state; + } +} + +// ============================================================================= +// Hook +// ============================================================================= + +export const useSendReducer = createReducerHook(sendReducer, sendActions, initialSendState); +``` + +- [ ] **Step 2: Create the send context** + +Create `src/contexts/send/SendContext.tsx`: + +```typescript +/** + * Send Context + * Manages offchain transfer flow and link generation + */ + +import { createContext, useContext, useCallback, type ReactNode } from 'react'; +import { AztecAddress } from '@aztec/aztec.js/addresses'; +import { useContracts } from '../contracts'; +import { useWallet } from '../wallet'; +import { useNetwork } from '../network'; +import { useSendReducer, type SendState } from './reducer'; +import { encodeTransferLink, type TransferLink } from '../../services/offchainLinkService'; +import { addSentTransfer } from '../../services/sentHistoryService'; +import { executeTransferOffchain } from '../../services/contractService'; + +interface SendContextType extends SendState { + canSend: boolean; + setToken: (token: 'gc' | 'gcp') => void; + setRecipientAddress: (address: string) => void; + setAmount: (amount: string) => void; + executeSend: () => Promise; + dismissError: () => void; + reset: () => void; +} + +const SendContext = createContext(undefined); + +export function useSend() { + const context = useContext(SendContext); + if (context === undefined) { + throw new Error('useSend must be used within a SendProvider'); + } + return context; +} + +interface SendProviderProps { + children: ReactNode; +} + +export function SendProvider({ children }: SendProviderProps) { + const { currentAddress, isUsingEmbeddedWallet } = useWallet(); + const { isLoadingContracts } = useContracts(); + const { activeNetwork } = useNetwork(); + const [state, actions] = useSendReducer(); + + const canSend = + !!state.amount && + parseFloat(state.amount) > 0 && + !!state.recipientAddress && + !isLoadingContracts && + !isUsingEmbeddedWallet && + !!currentAddress; + + const executeSend = useCallback(async () => { + if (!currentAddress || !state.recipientAddress || !state.amount) { + actions.sendError('Missing required fields'); + return; + } + + actions.startSend(); + + try { + const recipient = AztecAddress.fromString(state.recipientAddress); + const amount = BigInt(Math.round(parseFloat(state.amount))); + + const tokenKey = state.token === 'gc' ? 'gregoCoin' : 'gregoCoinPremium'; + const contractAddress = activeNetwork.contracts[tokenKey]; + + const { receipt, offchainMessages } = await executeTransferOffchain( + tokenKey, + currentAddress, + recipient, + amount, + ); + + actions.generatingLink(); + + // Encode the first recipient message into a link + const recipientMessage = offchainMessages[0]; + if (!recipientMessage) { + throw new Error('No offchain message generated for recipient'); + } + + const linkData: TransferLink = { + token: state.token, + amount: state.amount, + recipient: state.recipientAddress, + contractAddress, + txHash: receipt.txHash.toString(), + anchorBlockTimestamp: recipientMessage.anchorBlockTimestamp.toString(), + payload: recipientMessage.payload.map((f: { toString: () => string }) => f.toString()), + }; + + const link = encodeTransferLink(linkData); + actions.linkReady(link); + + // Save to history + addSentTransfer(currentAddress.toString(), { + id: receipt.txHash.toString(), + token: state.token, + amount: state.amount, + recipient: state.recipientAddress, + link, + createdAt: Date.now(), + status: 'confirmed', + }); + } catch (error) { + const message = error instanceof Error ? error.message : 'Send failed. Please try again.'; + actions.sendError(message); + } + }, [currentAddress, state.recipientAddress, state.amount, state.token, activeNetwork, actions]); + + const value: SendContextType = { + ...state, + canSend, + setToken: actions.setToken, + setRecipientAddress: actions.setRecipientAddress, + setAmount: actions.setAmount, + executeSend, + dismissError: actions.dismissError, + reset: actions.reset, + }; + + return {children}; +} +``` + +Note: The `executeTransferOffchain` function referenced above will be added to `contractService.ts` in Task 5. + +- [ ] **Step 3: Create index exports** + +Create `src/contexts/send/index.ts`: + +```typescript +export { SendProvider, useSend } from './SendContext'; +export type { SendPhase, SendState } from './reducer'; +``` + +- [ ] **Step 4: Add SendProvider to main.tsx** + +In `src/main.tsx`, add the import and wrap `` with `SendProvider` as a sibling to `SwapProvider`: + +```typescript +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { App } from './App.tsx'; +import { NetworkProvider } from './contexts/network/NetworkContext'; +import { WalletProvider } from './contexts/wallet/WalletContext'; +import { ContractsProvider } from './contexts/contracts/ContractsContext'; +import { SwapProvider } from './contexts/swap/SwapContext'; +import { SendProvider } from './contexts/send/SendContext'; +import { OnboardingProvider } from './contexts/onboarding/OnboardingContext'; + +createRoot(document.getElementById('root')!).render( + + + + + + + + + + + + + + + , +); +``` + +- [ ] **Step 5: Commit** + +```bash +git add src/contexts/send/ src/main.tsx +git commit -m "feat: add Send context with reducer, provider, and state machine" +``` + +--- + +### Task 5: Contract Service — executeTransferOffchain + +**Files:** +- Modify: `src/services/contractService.ts` + +This task adds the `executeTransferOffchain` function to the existing contract service. It needs to: +1. Call `transfer_offchain` on the token contract +2. Self-deliver the sender's change note via `offchain_receive` +3. Return the recipient's offchain messages for link encoding + +- [ ] **Step 1: Add the import for OffchainMessage** + +At the top of `src/services/contractService.ts`, add the import for the offchain message type. Check the exact import path from: +- `@aztec/aztec.js/contracts` exports `extractOffchainOutput` +- The `OffchainMessage` type comes from `@aztec/aztec.js/contracts` or `@aztec/aztec.js` + +Add alongside existing imports: + +```typescript +import type { OffchainMessage } from '@aztec/aztec.js/contracts'; +``` + +- [ ] **Step 2: Add executeTransferOffchain function** + +Add this function to `src/services/contractService.ts` after the existing `executeDrip` function: + +```typescript +/** + * Execute an offchain token transfer. + * Sends tokens privately with offchain note delivery, self-delivers the sender's + * change note, and returns the recipient's offchain messages for link encoding. + */ +export async function executeTransferOffchain( + tokenKey: 'gregoCoin' | 'gregoCoinPremium', + fromAddress: AztecAddress, + recipient: AztecAddress, + amount: bigint, + contracts: SwapContracts, +): Promise<{ receipt: TxReceipt; offchainMessages: OffchainMessage[] }> { + const token = tokenKey === 'gregoCoin' ? contracts.gregoCoin : contracts.gregoCoinPremium; + + // 1. Send the offchain transfer transaction + const { receipt, offchainMessages } = await token.methods + .transfer_offchain(recipient, amount) + .send({ from: fromAddress }); + + // 2. Self-deliver sender's change note (manual until F-324 lands) + const senderMessages = offchainMessages.filter( + (msg: OffchainMessage) => msg.recipient.equals(fromAddress), + ); + if (senderMessages.length > 0) { + await token.methods + .offchain_receive( + senderMessages.map((msg: OffchainMessage) => ({ + ciphertext: msg.payload, + recipient: fromAddress, + tx_hash: receipt.txHash.hash, + anchor_block_timestamp: msg.anchorBlockTimestamp, + })), + ) + .simulate({ from: fromAddress }); + } + + // 3. Filter and return recipient's messages for link encoding + const recipientMessages = offchainMessages.filter( + (msg: OffchainMessage) => msg.recipient.equals(recipient), + ); + + return { receipt, offchainMessages: recipientMessages }; +} +``` + +Note: The exact types for `offchainMessages` returned by `.send()` and the shape of the `offchain_receive` argument may need adjustment during implementation based on the SDK version. Check the `TxSendResultMined` type and `offchain_receive` ABI in the compiled contract artifacts. + +- [ ] **Step 3: Add parseSendError function** + +Add this after the existing `parseDripError` function: + +```typescript +export function parseSendError(error: unknown): string { + if (!(error instanceof Error)) return 'Send failed. Please try again.'; + const msg = error.message; + if (msg.includes('Balance too low')) return 'Insufficient token balance'; + if (msg.includes('User denied') || msg.includes('rejected')) return 'Transaction was rejected in wallet'; + if (msg.includes('invalid') && msg.includes('address')) return 'Invalid recipient address'; + return msg; +} +``` + +- [ ] **Step 4: Commit** + +```bash +git add src/services/contractService.ts +git commit -m "feat: add executeTransferOffchain to contract service + +Handles offchain transfer execution, sender change note self-delivery, +and recipient message extraction for link encoding." +``` + +--- + +### Task 6: Install QR Code Dependency + +**Files:** +- Modify: `package.json` + +- [ ] **Step 1: Install qrcode.react** + +```bash +yarn add qrcode.react +``` + +- [ ] **Step 2: Commit** + +```bash +git add package.json yarn.lock +git commit -m "chore: add qrcode.react dependency for transfer link QR codes" +``` + +--- + +### Task 7: Send UI Components + +**Files:** +- Create: `src/components/send/SendForm.tsx` +- Create: `src/components/send/SendProgress.tsx` +- Create: `src/components/send/LinkDisplay.tsx` +- Create: `src/components/send/SentHistory.tsx` +- Create: `src/components/send/SendContainer.tsx` + +- [ ] **Step 1: Create SendForm** + +Create `src/components/send/SendForm.tsx`: + +```tsx +import { Box, TextField, Typography, ToggleButton, ToggleButtonGroup, Button } from '@mui/material'; +import { useSend } from '../../contexts/send'; + +interface SendFormProps { + balance: { gc: bigint | null; gcp: bigint | null }; +} + +export function SendForm({ balance }: SendFormProps) { + const { token, recipientAddress, amount, canSend, setToken, setRecipientAddress, setAmount, executeSend, phase } = + useSend(); + + const isSending = phase === 'sending' || phase === 'generating_link'; + const currentBalance = token === 'gc' ? balance.gc : balance.gcp; + + return ( + + {/* Token Selector */} + + + Token + + value && setToken(value)} + size="small" + fullWidth + disabled={isSending} + > + GregoCoin + GregoCoinPremium + + + + {/* Recipient Address */} + setRecipientAddress(e.target.value)} + fullWidth + disabled={isSending} + size="small" + /> + + {/* Amount */} + + setAmount(e.target.value)} + fullWidth + disabled={isSending} + size="small" + slotProps={{ + input: { + endAdornment: currentBalance !== null ? ( + + Balance: {currentBalance.toString()} + + ) : null, + }, + }} + /> + + + {/* Send Button */} + + + ); +} +``` + +- [ ] **Step 2: Create SendProgress** + +Create `src/components/send/SendProgress.tsx`: + +```tsx +import { Box, Typography, CircularProgress } from '@mui/material'; +import type { SendPhase } from '../../contexts/send'; + +interface SendProgressProps { + phase: SendPhase; +} + +const phaseMessages: Record = { + sending: 'Sending transaction...', + generating_link: 'Generating claim link...', +}; + +export function SendProgress({ phase }: SendProgressProps) { + const message = phaseMessages[phase]; + if (!message) return null; + + return ( + + + + {message} + + + ); +} +``` + +- [ ] **Step 3: Create LinkDisplay** + +Create `src/components/send/LinkDisplay.tsx`: + +```tsx +import { Box, Typography, Button, IconButton, Snackbar } from '@mui/material'; +import ContentCopyIcon from '@mui/icons-material/ContentCopy'; +import { QRCodeSVG } from 'qrcode.react'; +import { useState } from 'react'; + +interface LinkDisplayProps { + link: string; + amount: string; + token: 'gc' | 'gcp'; + recipient: string; + onReset: () => void; +} + +export function LinkDisplay({ link, amount, token, recipient, onReset }: LinkDisplayProps) { + const [copied, setCopied] = useState(false); + const tokenName = token === 'gc' ? 'GregoCoin' : 'GregoCoinPremium'; + + const handleCopy = async () => { + await navigator.clipboard.writeText(link); + setCopied(true); + }; + + return ( + + + Sent! + + + {amount} {tokenName} → {recipient.slice(0, 8)}...{recipient.slice(-4)} + + + {/* Copyable Link */} + + + {link} + + + + + + + {/* QR Code */} + + + + + Scan to claim + + + + + setCopied(false)} + message="Link copied!" + /> + + ); +} +``` + +- [ ] **Step 4: Create SentHistory** + +Create `src/components/send/SentHistory.tsx`: + +```tsx +import { Box, Typography, IconButton, Collapse, Snackbar, Chip } from '@mui/material'; +import ContentCopyIcon from '@mui/icons-material/ContentCopy'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import { useState } from 'react'; +import { getSentTransfers, type SentTransfer } from '../../services/sentHistoryService'; + +interface SentHistoryProps { + senderAddress: string; +} + +function timeAgo(timestamp: number): string { + const seconds = Math.floor((Date.now() - timestamp) / 1000); + if (seconds < 60) return `${seconds}s ago`; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}m ago`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h ago`; + return `${Math.floor(hours / 24)}d ago`; +} + +function StatusChip({ status }: { status: SentTransfer['status'] }) { + if (status === 'confirmed') return null; + const color = status === 'pending' ? 'warning' : 'error'; + return ; +} + +export function SentHistory({ senderAddress }: SentHistoryProps) { + const [copied, setCopied] = useState(false); + const [expanded, setExpanded] = useState(false); + const transfers = getSentTransfers(senderAddress); + + if (transfers.length === 0) return null; + + const visibleTransfers = expanded ? transfers : transfers.slice(0, 3); + const hasMore = transfers.length > 3; + + const handleCopy = async (link: string) => { + await navigator.clipboard.writeText(link); + setCopied(true); + }; + + return ( + + + Sent transfers + + {visibleTransfers.map(transfer => ( + + + + {transfer.amount} {transfer.token === 'gc' ? 'GC' : 'GCP'} + + + → {transfer.recipient.slice(0, 8)}...{transfer.recipient.slice(-4)} + + + + + + {timeAgo(transfer.createdAt)} + + handleCopy(transfer.link)}> + + + + + ))} + {hasMore && ( + + setExpanded(!expanded)} + sx={{ transform: expanded ? 'rotate(180deg)' : 'none', transition: '0.2s' }} + > + + + + )} + setCopied(false)} message="Link copied!" /> + + ); +} +``` + +- [ ] **Step 5: Create SendContainer** + +Create `src/components/send/SendContainer.tsx`. This orchestrates the send flow, similar to how `SwapContainer` orchestrates swaps: + +```tsx +import { Box, Alert } from '@mui/material'; +import { useSend } from '../../contexts/send'; +import { useWallet } from '../../contexts/wallet'; +import { useContracts } from '../../contexts/contracts'; +import { SendForm } from './SendForm'; +import { SendProgress } from './SendProgress'; +import { LinkDisplay } from './LinkDisplay'; +import { SentHistory } from './SentHistory'; +import { useEffect, useState } from 'react'; + +export function SendContainer() { + const { phase, error, generatedLink, token, amount, recipientAddress, dismissError, reset } = useSend(); + const { currentAddress, isUsingEmbeddedWallet } = useWallet(); + const { fetchBalances } = useContracts(); + const [balances, setBalances] = useState<{ gc: bigint | null; gcp: bigint | null }>({ + gc: null, + gcp: null, + }); + + useEffect(() => { + if (currentAddress && !isUsingEmbeddedWallet) { + fetchBalances().then(([gc, gcp]) => setBalances({ gc, gcp })); + } + }, [currentAddress, isUsingEmbeddedWallet, fetchBalances]); + + // Refresh balances after a successful send + useEffect(() => { + if (phase === 'link_ready' && currentAddress) { + fetchBalances().then(([gc, gcp]) => setBalances({ gc, gcp })); + } + }, [phase, currentAddress, fetchBalances]); + + if (isUsingEmbeddedWallet) { + return ( + + Connect an external wallet to send tokens. + + ); + } + + return ( + + {phase === 'link_ready' && generatedLink ? ( + + ) : ( + <> + + + + )} + + {error && ( + + {error} + + )} + + {currentAddress && } + + ); +} +``` + +- [ ] **Step 6: Commit** + +```bash +git add src/components/send/ +git commit -m "feat: add Send UI components + +SendForm (token selector, address, amount), SendProgress, LinkDisplay +(copyable link + QR code), SentHistory, and SendContainer orchestrator." +``` + +--- + +### Task 8: Claim Page Components + +**Files:** +- Create: `src/components/claim/ClaimProgress.tsx` +- Create: `src/components/claim/ClaimSuccess.tsx` +- Create: `src/components/claim/ClaimPage.tsx` + +- [ ] **Step 1: Create ClaimProgress** + +Create `src/components/claim/ClaimProgress.tsx`: + +```tsx +import { Box, Typography, CircularProgress } from '@mui/material'; + +type ClaimPhase = 'claiming' | 'verifying'; + +interface ClaimProgressProps { + phase: ClaimPhase; +} + +const phaseMessages: Record = { + claiming: 'Claiming tokens...', + verifying: 'Verifying amount...', +}; + +export function ClaimProgress({ phase }: ClaimProgressProps) { + return ( + + + + {phaseMessages[phase]} + + + ); +} +``` + +- [ ] **Step 2: Create ClaimSuccess** + +Create `src/components/claim/ClaimSuccess.tsx`: + +```tsx +import { Box, Typography, Button, Chip } from '@mui/material'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; + +interface ClaimSuccessProps { + amount: string; + tokenName: string; + verified: boolean; + onGoToSwap: () => void; +} + +export function ClaimSuccess({ amount, tokenName, verified, onGoToSwap }: ClaimSuccessProps) { + return ( + + + + Tokens Claimed! + + + + {amount} {tokenName} + + + + + + ); +} +``` + +- [ ] **Step 3: Create ClaimPage** + +Create `src/components/claim/ClaimPage.tsx`. This is the main claim flow orchestrator: + +```tsx +import { Box, Typography, Button, Alert, CircularProgress, Container, Chip } from '@mui/material'; +import { useEffect, useState, useCallback } from 'react'; +import { useWallet } from '../../contexts/wallet'; +import { useContracts } from '../../contexts/contracts'; +import { extractClaimPayload, type TransferLink } from '../../services/offchainLinkService'; +import { ClaimProgress } from './ClaimProgress'; +import { ClaimSuccess } from './ClaimSuccess'; +import { GregoSwapLogo } from '../GregoSwapLogo'; + +type ClaimState = + | { phase: 'decoding' } + | { phase: 'preview'; data: TransferLink } + | { phase: 'claiming'; data: TransferLink } + | { phase: 'verifying'; data: TransferLink } + | { phase: 'claimed'; data: TransferLink; verified: boolean } + | { phase: 'error'; message: string }; + +export function ClaimPage() { + const [state, setState] = useState({ phase: 'decoding' }); + const { wallet, currentAddress, isUsingEmbeddedWallet } = useWallet(); + const { fetchBalances, registerBaseContracts, isLoadingContracts } = useContracts(); + + // Step 1: Decode the link on mount + useEffect(() => { + const data = extractClaimPayload(); + if (!data) { + setState({ phase: 'error', message: 'Invalid or missing claim link.' }); + return; + } + setState({ phase: 'preview', data }); + }, []); + + // Step 2: Execute the claim + const doClaim = useCallback(async () => { + if (state.phase !== 'preview') return; + const { data } = state; + + setState({ phase: 'claiming', data }); + + try { + // Ensure contracts are registered + if (!isLoadingContracts && wallet) { + await registerBaseContracts(); + } + + // Wait for wallet to be ready + if (!wallet || !currentAddress) { + // Wallet should auto-create (embedded) or already be connected + setState({ phase: 'error', message: 'No wallet available. Please refresh and try again.' }); + return; + } + + // Get balance before claim (for verification diff) + let balanceBefore = 0n; + try { + const [gc, gcp] = await fetchBalances(); + balanceBefore = data.token === 'gc' ? gc : gcp; + } catch { + // New wallet might not have balance yet — that's fine + } + + // Reconstruct Fr values from payload strings + const { Fr } = await import('@aztec/aztec.js/fields'); + const payload = data.payload.map((s: string) => Fr.fromString(s)); + + // Call offchain_receive on the token contract + const tokenKey = data.token === 'gc' ? 'gregoCoin' : 'gregoCoinPremium'; + // Access the token contract from the contracts context + // Note: this will need to be adapted based on how contracts are exposed + const { AztecAddress } = await import('@aztec/aztec.js/addresses'); + const recipient = AztecAddress.fromString(data.recipient); + + // The actual offchain_receive call — this needs the token contract instance. + // The ContractsContext will need to expose the token contracts or a claimOffchain method. + // For now, show the pattern: + // await tokenContract.methods.offchain_receive([{ + // ciphertext: payload, + // recipient, + // tx_hash: data.txHash, + // anchor_block_timestamp: BigInt(data.anchorBlockTimestamp), + // }]).simulate({ from: currentAddress }); + + setState({ phase: 'verifying', data }); + + // Verify: check balance after + const [gcAfter, gcpAfter] = await fetchBalances(); + const balanceAfter = data.token === 'gc' ? gcAfter : gcpAfter; + const received = balanceAfter - balanceBefore; + const expectedAmount = BigInt(Math.round(parseFloat(data.amount))); + const verified = received >= expectedAmount; + + setState({ phase: 'claimed', data, verified }); + } catch (error) { + const message = error instanceof Error ? error.message : 'Claim failed. Please try again.'; + setState({ phase: 'error', message }); + } + }, [state, wallet, currentAddress, isLoadingContracts, registerBaseContracts, fetchBalances]); + + const handleGoToSwap = () => { + window.location.hash = ''; + window.location.reload(); + }; + + const tokenName = (t: string) => (t === 'gc' ? 'GregoCoin' : 'GregoCoinPremium'); + + return ( + + + + + + + {state.phase === 'decoding' && ( + + + + )} + + {state.phase === 'preview' && ( + + + Someone sent you + + + + {state.data.amount} {tokenName(state.data.token)} + + + + + + )} + + {state.phase === 'claiming' && } + {state.phase === 'verifying' && } + + {state.phase === 'claimed' && ( + + )} + + {state.phase === 'error' && ( + {state.message} + )} + + + ); +} +``` + +**Implementation note:** The `offchain_receive` call in `ClaimPage` is commented as a pattern because the `ContractsContext` doesn't currently expose raw token contract instances. During implementation, either: +1. Expose the token contracts from `ContractsContext` (add a `getTokenContract(tokenKey)` method), or +2. Add a `claimOffchainTransfer(tokenKey, message)` method to `ContractsContext` + +Option 2 is cleaner — it follows the existing pattern where `ContractsContext` wraps contract interactions. + +- [ ] **Step 4: Commit** + +```bash +git add src/components/claim/ +git commit -m "feat: add Claim page components + +ClaimPage (orchestrator with state machine), ClaimProgress, +and ClaimSuccess. Handles link decoding, wallet resolution, +offchain_receive, and balance verification." +``` + +--- + +### Task 9: App Routing and Tab Bar + +**Files:** +- Modify: `src/components/App.tsx` + +- [ ] **Step 1: Add route detection and tab bar to App.tsx** + +Update `src/components/App.tsx` to: +1. Detect `/#/claim/` routes and render `ClaimPage` instead of the main UI +2. Add a Swap/Send tab bar + +Replace the entire file with: + +```tsx +import { ThemeProvider, CssBaseline, Container, Box, Typography, Tabs, Tab } from '@mui/material'; +import { theme } from './theme'; +import { GregoSwapLogo } from './components/GregoSwapLogo'; +import { WalletChip } from './components/WalletChip'; +import { NetworkSwitcher } from './components/NetworkSwitcher'; +import { FooterInfo } from './components/FooterInfo'; +import { SwapContainer } from './components/swap'; +import { SendContainer } from './components/send/SendContainer'; +import { ClaimPage } from './components/claim/ClaimPage'; +import { useWallet } from './contexts/wallet'; +import { useOnboarding } from './contexts/onboarding'; +import { OnboardingModal } from './components/OnboardingModal'; +import { TxNotificationCenter } from './components/TxNotificationCenter'; +import { isClaimRoute } from './services/offchainLinkService'; +import type { AztecAddress } from '@aztec/aztec.js/addresses'; +import { useState } from 'react'; + +export function App() { + const { disconnectWallet, setCurrentAddress, currentAddress, error: walletError, isLoading: walletLoading } = + useWallet(); + const { isOnboardingModalOpen, startOnboarding, resetOnboarding, status: onboardingStatus } = useOnboarding(); + const [activeTab, setActiveTab] = useState(0); + + const isOnboarded = onboardingStatus === 'completed'; + + // If on a claim route, render the claim page directly + if (isClaimRoute()) { + return ( + + + + + + + ); + } + + const handleWalletClick = () => { + if (isOnboarded && currentAddress) { + resetOnboarding(); + } + startOnboarding(); + }; + + const handleDisconnect = async () => { + await disconnectWallet(); + resetOnboarding(); + }; + + return ( + + + + + + + + + + + + + + Swap GregoCoin for GregoCoinPremium + + + + {/* Tab Bar */} + setActiveTab(value)} + centered + sx={{ + mb: 3, + '& .MuiTab-root': { color: 'text.secondary', fontWeight: 600 }, + '& .Mui-selected': { color: 'primary.main' }, + '& .MuiTabs-indicator': { backgroundColor: 'primary.main' }, + }} + > + + + + + {/* Tab Content */} + {activeTab === 0 && } + {activeTab === 1 && } + + {walletError && ( + + + + Wallet Connection Error + + + {walletError} + + + + )} + + {walletLoading && !walletError && ( + + + + Connecting to network... + + + + )} + + + + + + { + setCurrentAddress(address); + }} + /> + + + + ); +} +``` + +- [ ] **Step 2: Verify build compiles** + +```bash +yarn build +``` + +Expected: Build succeeds (or only type errors from the `offchain_receive` integration in ClaimPage, which will be finalized during integration testing). + +- [ ] **Step 3: Commit** + +```bash +git add src/components/App.tsx +git commit -m "feat: add hash routing for claim page and Swap/Send tab bar + +Detects /#/claim/ routes and renders ClaimPage. Adds Swap/Send +tabs to the main interface." +``` + +--- + +### Task 10: Integration — Wire ContractsContext for Offchain Transfers + +**Files:** +- Modify: `src/contexts/contracts/ContractsContext.tsx` + +The `ClaimPage` and `SendContext` need to interact with token contracts for `transfer_offchain` and `offchain_receive`. The cleanest approach is to add methods to `ContractsContext`. + +- [ ] **Step 1: Add sendOffchain and claimOffchainTransfer to ContractsContextType** + +In `src/contexts/contracts/ContractsContext.tsx`, add to the `ContractsContextType` interface: + +```typescript + // Offchain transfer methods + sendOffchain: (tokenKey: 'gregoCoin' | 'gregoCoinPremium', recipient: AztecAddress, amount: bigint) => Promise<{ receipt: TxReceipt; offchainMessages: any[] }>; + claimOffchainTransfer: (tokenKey: 'gregoCoin' | 'gregoCoinPremium', message: { ciphertext: any[]; recipient: AztecAddress; tx_hash: string; anchor_block_timestamp: bigint }) => Promise; +``` + +- [ ] **Step 2: Implement the methods in ContractsProvider** + +Add the implementations inside `ContractsProvider`, following the pattern of existing methods like `swap`: + +```typescript + const sendOffchain = useCallback(async ( + tokenKey: 'gregoCoin' | 'gregoCoinPremium', + recipient: AztecAddress, + amount: bigint, + ) => { + if (!wallet || !currentAddress || !state.contracts) { + throw new Error('Contracts not initialized'); + } + return contractService.executeTransferOffchain( + tokenKey, + currentAddress, + recipient, + amount, + state.contracts, + ); + }, [wallet, currentAddress, state.contracts]); + + const claimOffchainTransfer = useCallback(async ( + tokenKey: 'gregoCoin' | 'gregoCoinPremium', + message: { ciphertext: any[]; recipient: AztecAddress; tx_hash: string; anchor_block_timestamp: bigint }, + ) => { + if (!wallet || !currentAddress || !state.contracts) { + throw new Error('Contracts not initialized'); + } + const token = tokenKey === 'gregoCoin' ? state.contracts.gregoCoin : state.contracts.gregoCoinPremium; + await token.methods + .offchain_receive([message]) + .simulate({ from: currentAddress }); + }, [wallet, currentAddress, state.contracts]); +``` + +Add both methods to the context value object. + +- [ ] **Step 3: Update SendContext to use sendOffchain from ContractsContext** + +In `src/contexts/send/SendContext.tsx`, replace the direct `executeTransferOffchain` call with: + +```typescript +const { sendOffchain } = useContracts(); + +// Inside executeSend: +const { receipt, offchainMessages } = await sendOffchain(tokenKey, recipient, amount); +``` + +Remove the direct import of `executeTransferOffchain` from `contractService`. + +- [ ] **Step 4: Update ClaimPage to use claimOffchainTransfer** + +In `src/components/claim/ClaimPage.tsx`, replace the commented-out `offchain_receive` call with: + +```typescript +const { claimOffchainTransfer, registerBaseContracts, fetchBalances } = useContracts(); + +// Inside doClaim: +const { Fr } = await import('@aztec/aztec.js/fields'); +const { AztecAddress } = await import('@aztec/aztec.js/addresses'); + +await claimOffchainTransfer( + data.token === 'gc' ? 'gregoCoin' : 'gregoCoinPremium', + { + ciphertext: data.payload.map((s: string) => Fr.fromString(s)), + recipient: AztecAddress.fromString(data.recipient), + tx_hash: data.txHash, + anchor_block_timestamp: BigInt(data.anchorBlockTimestamp), + }, +); +``` + +- [ ] **Step 5: Commit** + +```bash +git add src/contexts/contracts/ContractsContext.tsx src/contexts/send/SendContext.tsx src/components/claim/ClaimPage.tsx +git commit -m "feat: wire offchain transfer methods through ContractsContext + +Add sendOffchain and claimOffchainTransfer to ContractsContext. +Update SendContext and ClaimPage to use them." +``` + +--- + +### Task 11: Deploy and End-to-End Test + +**Files:** No new files — integration testing + +- [ ] **Step 1: Deploy contracts to local sandbox** + +```bash +# Terminal 1: Start Aztec sandbox (if not running) +aztec start --local-network + +# Terminal 2: Deploy contracts with the forked token +PASSWORD=test123 yarn deploy:local +``` + +Expected: All contracts deploy successfully, including the forked Token contract with `transfer_offchain`. + +- [ ] **Step 2: Start dev server and test the send flow** + +```bash +yarn serve +``` + +1. Open the app, connect an external wallet, complete onboarding +2. Switch to the "Send" tab +3. Select GregoCoin, enter recipient address (use a second account), enter amount +4. Click "Send & Generate Link" +5. Verify: Transaction sends, link is generated, QR code appears +6. Copy the link + +- [ ] **Step 3: Test the claim flow** + +1. Open the copied link in a new browser tab/incognito window +2. Verify: Preview shows "Someone sent you X GregoCoin" with "unverified" badge +3. Click "Claim" +4. Verify: Wallet auto-creates, offchain_receive is called, balance is verified +5. Verify: Success screen with verified amount + +- [ ] **Step 4: Test sent history** + +1. Go back to the sender's tab, check the Send tab +2. Verify: Sent history shows the transfer with "confirmed" status +3. Click "Copy link" — verify it copies the same link + +- [ ] **Step 5: Test error cases** + +1. Try opening a claim link with a wallet whose address doesn't match the recipient — should show decryption error +2. Try sending with insufficient balance — should show balance error +3. Try claim with an invalid/corrupted link — should show invalid link error + +- [ ] **Step 6: Commit any fixes from testing** + +```bash +git add -A +git commit -m "fix: integration adjustments from end-to-end testing" +``` + +--- + +### Task 12: Redeploy Contracts (if needed) + +If the compiled Token contract artifact changed (new ABI from `transfer_offchain`), the deploy script may need updating to reference the new artifact. Check: + +- [ ] **Step 1: Verify contract artifacts** + +```bash +ls contracts/target/ +``` + +Check that `Token.json` (or equivalent artifact) includes the `transfer_offchain` method in its ABI. + +- [ ] **Step 2: Update deploy script if needed** + +Check the deploy script (`deploy:local` in package.json) to ensure it deploys the forked Token contract. Since we kept the same contract name (`token_contract`), the artifacts should be compatible. + +- [ ] **Step 3: Update deployed-addresses.json if needed** + +If contract addresses change after redeployment, update `src/config/networks/deployed-addresses.json` with new addresses. + +- [ ] **Step 4: Commit** + +```bash +git add contracts/target/ src/config/networks/ +git commit -m "chore: update contract artifacts and deployed addresses for forked token" +``` diff --git a/docs/superpowers/specs/2026-04-09-p2p-offchain-transfers-design.md b/docs/superpowers/specs/2026-04-09-p2p-offchain-transfers-design.md new file mode 100644 index 0000000..681bbe3 --- /dev/null +++ b/docs/superpowers/specs/2026-04-09-p2p-offchain-transfers-design.md @@ -0,0 +1,373 @@ +# P2P Private Transfers with Offchain Delivery + +**Date:** 2026-04-09 +**Status:** Design approved +**Goal:** Add P2P private token transfers to GregoSwap using Aztec's offchain delivery feature, with shareable claim links and QR codes as the delivery channel. + +## Motivation + +This feature serves as a dogfooding vehicle for Aztec's offchain delivery feature. It exercises the full vertical slice: + +- **Contract DX:** Writing `.deliver(MessageDelivery.OFFCHAIN)` in a forked Token contract +- **SDK integration:** Extracting `offchainMessages` from transactions, calling `offchain_receive()` +- **Delivery channel:** Encoding offchain messages into shareable URLs and QR codes +- **End-user experience:** Sender generates a link, recipient opens it and claims tokens + +Lessons learned will be retrofitted into the offchain delivery feature itself. + +## Design Decisions + +| Decision | Choice | Rationale | +|----------|--------|-----------| +| Delivery channel | Shareable link + QR code | Tests URL-based delivery; works for both remote sharing and in-person | +| Token note delivery | Offchain (not escrow) | Exercises the core offchain delivery path; note is created directly for recipient | +| Expiration | None (protocol-level tx expiry only) | Keeps focus on offchain delivery; escrow would bypass it | +| Transferable tokens | GregoCoin and GregoCoinPremium | Both tokens supported via token selector | +| Recipient wallet | Use existing if connected; auto-create embedded if not | Minimum friction for new users | +| Claim trigger | Explicit "Claim" button | Gives recipient time to understand what's happening before committing | +| Amount display | Show URL amount optimistically, verify after claim | Instant preview with trust-but-verify UX | +| Contract change | Local fork of Token contract | Iterate locally; upstream later if it works well | +| Transfer event | Offchain (same as notes) | Fully consistent — nothing onchain except the note hash | + +## Non-Goals + +- No expiration/reclaim mechanism (tokens are gone once the note is in the tree) +- No claim detection from sender's perspective (fire-and-forget by design) +- No address book or ENS-style resolution +- No "send to anyone" pattern (recipient address is required for encryption) + +## Architecture + +### Overview + +``` +Sender Recipient + │ │ + ├─ transfer_offchain(to, amt) │ + │ ├─ subtract sender balance │ + │ ├─ add recipient note ──── .deliver(OFFCHAIN) + │ ├─ add sender change note ─ .deliver(OFFCHAIN) + │ └─ emit Transfer event ──── .deliver(OFFCHAIN) + │ │ + ├─ SDK returns offchainMessages │ + ├─ Self-deliver change note │ + ├─ Encode recipient msg → URL │ + ├─ Show link + QR code │ + │ │ + │ ─── share link ───────────────>│ + │ ├─ Open link → preview amount + │ ├─ Click "Claim" + │ ├─ Connect/create wallet + │ ├─ offchain_receive(message) + │ ├─ PXE syncs → note decrypted + │ ├─ Verify balance + │ └─ Tokens available +``` + +### File Structure + +``` +contracts/ + token/ # NEW — fork of standard Token contract + src/main.nr # standard Token + transfer_offchain method + Nargo.toml + amm/Nargo.toml # MOD — point token dep to local fork + proof_of_password/Nargo.toml # MOD — point token dep to local fork + +src/ + services/ + offchainLinkService.ts # NEW — encode/decode transfer links + sentHistoryService.ts # NEW — localStorage CRUD for sent transfers + contractService.ts # MOD — add executeTransferOffchain + components/ + App.tsx # MOD — route detection, tab bar + send/ + SendContainer.tsx # NEW — orchestrates send flow + SendForm.tsx # NEW — token selector, address, amount + SendProgress.tsx # NEW — sending + generating states + LinkDisplay.tsx # NEW — copyable link + QR code + SentHistory.tsx # NEW — list of sent transfers + claim/ + ClaimPage.tsx # NEW — orchestrates claim flow + ClaimProgress.tsx # NEW — state machine progress + ClaimSuccess.tsx # NEW — success state with CTA + swap/ + SwapContainer.tsx # MOD — wrap in tab structure + contexts/ + send/ + SendContext.tsx # NEW — send flow state management + reducer.ts # NEW — send state machine + index.ts +``` + +## Section 1: Smart Contract + +Fork the standard Token contract into `contracts/token/`. The only addition is a `transfer_offchain` method — identical to `transfer` but with `MessageDelivery.OFFCHAIN` for all deliveries. + +### New method + +```noir +#[external("private")] +fn transfer_offchain(to: AztecAddress, amount: u128) { + let from = self.msg_sender(); + + let change = self.internal.subtract_balance(from, amount, INITIAL_TRANSFER_CALL_MAX_NOTES); + self.storage.balances.at(from).add(change).deliver(MessageDelivery.OFFCHAIN); + self.storage.balances.at(to).add(amount).deliver(MessageDelivery.OFFCHAIN); + + self.emit(Transfer { from, to, amount }).deliver_to( + to, + MessageDelivery.OFFCHAIN, + ); +} +``` + +### What changes vs. standard Token + +- **Add** `transfer_offchain` — new private function (~10 lines) +- **Add** `MessageDelivery` import (if not already present) +- **No changes** to `subtract_balance`, storage, other methods, or recursive balance logic + +### Dependency updates + +Update `Nargo.toml` in `contracts/amm/` and `contracts/proof_of_password/` to point `token` dependency to the local fork: + +```toml +token = { path = "../token" } +``` + +## Section 2: SDK Integration & Link Encoding + +### 2a. Extracting offchain messages + +The SDK's `.send()` already returns `{ receipt, offchainEffects, offchainMessages }` (type `TxSendResultMined`). No extra SDK work needed. + +New function in `contractService.ts`: + +```typescript +async function executeTransferOffchain( + token: TokenContract, + fromAddress: AztecAddress, + recipient: AztecAddress, + amount: bigint, +): Promise<{ receipt: TxReceipt; offchainMessages: OffchainMessage[] }> { + // 1. Send transaction — SDK extracts offchain messages automatically + const { receipt, offchainMessages } = await token.methods + .transfer_offchain(recipient, amount) + .send({ from: fromAddress }); + + // 2. Self-deliver sender's change note + const senderMessages = offchainMessages + .filter(msg => msg.recipient.equals(fromAddress)); + if (senderMessages.length > 0) { + await token.methods + .offchain_receive(senderMessages.map(msg => ({ + ciphertext: msg.payload, + recipient: fromAddress, + tx_hash: receipt.txHash.hash, + anchor_block_timestamp: msg.anchorBlockTimestamp, + }))) + .simulate({ from: fromAddress }); + } + + // 3. Return recipient's messages for link encoding + const recipientMessages = offchainMessages + .filter(msg => msg.recipient.equals(recipient)); + + return { receipt, offchainMessages: recipientMessages }; +} +``` + +### 2b. Link encoding + +New file: `src/services/offchainLinkService.ts` + +```typescript +interface TransferLink { + token: 'gc' | 'gcp'; // which token + amount: string; // human-readable amount (untrusted, for preview) + recipient: string; // intended recipient Aztec address + contractAddress: string; // token contract address + txHash: string; // originating tx hash + anchorBlockTimestamp: string; // for offchain_receive + payload: string[]; // Fr[] as hex strings (encrypted note ciphertext) +} + +function encodeTransferLink(data: TransferLink): string { + const json = JSON.stringify(data); + const encoded = btoa(json) + .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); + return `${window.location.origin}/#/claim/${encoded}`; +} + +function decodeTransferLink(encoded: string): TransferLink { + const base64 = encoded.replace(/-/g, '+').replace(/_/g, '/'); + return JSON.parse(atob(base64)); +} +``` + +### 2c. URL size estimate + +| Component | Size (approx) | +|-----------|---------------| +| Payload (Fr[] encrypted ciphertext) | ~20 fields x 64 hex chars = ~1,280 chars | +| Metadata (token, amount, addresses, tx hash) | ~300 chars | +| Base64 overhead (~33%) | ~520 chars | +| **Total URL length** | **~2,100 chars** | + +Within limits for browsers (~8,000 chars) and QR codes (~4,296 alphanumeric chars). + +## Section 3: Claim Flow (Recipient UX) + +### Route + +New hash route: `/#/claim/{base64url_payload}` + +`App.tsx` detects this route and renders `ClaimPage` instead of the swap interface. + +### State machine + +``` +decoding → preview → claiming → verifying → claimed + ↗ + error (from any state) +``` + +| State | What happens | User sees | +|-------|-------------|-----------| +| `decoding` | Parse base64url from URL, validate structure | Brief loading flash | +| `preview` | Display transfer info from URL metadata | "Someone sent you 50 GregoCoin!" with amount and token. **"Claim" button** | +| `claiming` | User clicked Claim. Connect/create wallet, register token contract, call `offchain_receive()` | "Claiming tokens..." spinner | +| `verifying` | Query `balance_of_private()`, compare to expected amount | "Verifying amount..." indicator (amount badge updates) | +| `claimed` | Balance confirmed | Amount badge turns green, "Tokens claimed! Start swapping" CTA | +| `error` | Invalid link, network error, amount mismatch, decryption failure | Error message with description | + +### Wallet resolution + +``` +On "Claim" button press: +├── External wallet already connected? +│ └── YES → Use it. Register token contract if needed. +│ +└── No wallet connected? + └── Auto-create embedded wallet + → Create node client + → Create embedded wallet + → Register token contract + → Claim into embedded wallet's address +``` + +The link's `recipient` field must match the claiming wallet's address — `offchain_receive` will fail to decrypt if they don't match. This surfaces as a clear error. + +### Amount verification + +The claim page shows the amount from the URL immediately (optimistic preview). After `offchain_receive` completes, it queries the balance and confirms the amount matches. The amount badge transitions from "unverified" to "verified" state. + +For new users (fresh embedded wallet): balance = received amount. +For returning users: snapshot balance before claim, diff after. + +### After claiming + +Success screen with CTA to navigate to the main swap page. If using an auto-created embedded wallet, the user is considered onboarded — skip the normal onboarding flow. + +## Section 4: Sender UX + +### Navigation + +Add a **Swap / Send** tab bar above the current swap interface. Both tabs share the same container width and visual style. The Send tab is only enabled when the user has a connected external wallet with a balance. + +### Send form + +- **Token toggle:** GregoCoin / GregoCoinPremium (two buttons, not a dropdown) +- **Recipient address:** Text input accepting a full Aztec address (hex string) +- **Amount:** Number input with balance display +- **Button:** "Send & Generate Link" + +### Send flow state machine + +``` +idle → sending → generating_link → link_ready +``` + +| State | What happens | +|-------|-------------| +| `idle` | Form visible, user fills in token + recipient + amount | +| `sending` | Call `transfer_offchain()`, wait for tx receipt + offchain messages | +| `generating_link` | Self-deliver change note, filter recipient messages, encode URL, generate QR | +| `link_ready` | Show copyable link + QR code, save to sent history | + +### Link display + +After generation: +- Copyable link field with copy button +- QR code (using `qrcode.react` or similar lightweight library — new dependency) +- "Send another" button to reset the form + +## Section 5: Sent History + +### Location + +Below the Send form, visible only on the Send tab. Collapsed by default if more than 3 entries. + +### Data model + +```typescript +interface SentTransfer { + id: string; // tx hash + token: 'gc' | 'gcp'; + amount: string; + recipient: string; // Aztec address + link: string; // full claim URL + createdAt: number; // timestamp + status: 'pending' | 'confirmed' | 'expired'; +} +``` + +Stored in localStorage keyed by sender address: `gregoswap_sent_transfers_{senderAddress}` + +### Status tracking + +Aztec transactions have a protocol-level 24-hour mining window. The tx hash is derived from the expiration timestamp, so we can determine the deadline without extra queries. + +| Status | Meaning | +|--------|---------| +| `pending` | Tx sent but not yet confirmed as mined | +| `confirmed` | Tx mined, note exists in the tree, link is valid and claimable | +| `expired` | 24h passed without the tx being mined (reorg, network issues, etc.) — tokens were never sent | + +The happy path is always `pending → confirmed` (typically within seconds/minutes). The `expired` state is an edge case (reorgs, sequencer issues) but important for informing the user that their tokens weren't actually sent. + +**Status resolution:** When the Send tab loads, pending transfers are checked by querying the node for the tx receipt (using the stored tx hash). If the tx is mined, status updates to `confirmed`. If the current time exceeds the 24h deadline derived from the tx hash, status updates to `expired`. + +**Claim detection is not possible** with the direct transfer approach. Once the tx is mined, the sender has no on-chain way to know if the recipient called `offchain_receive`. The history serves as a "links I've generated" log with re-share capability, not a live status tracker. + +### List UI + +Each row shows: +- Token amount and type (e.g., "50 GC") +- Truncated recipient address +- Relative timestamp +- Status indicator (for pending/expired states) +- "Copy link" button for re-sharing + +## New Dependencies + +- `qrcode.react` (or similar) — QR code generation for claim links + +## Dogfooding Observations (Captured During Design) + +These are insights surfaced during the design process, worth feeding back into the offchain delivery feature: + +1. **Partial notes don't support offchain delivery.** The `partial_note.complete()` flow hardcodes `ONCHAIN_UNCONSTRAINED`. This means most DeFi contracts (AMMs, lending) that use partial notes can't adopt offchain delivery without foundational changes. Offchain delivery currently only works with direct note creation. + +2. **Standard Token contract hardcodes delivery mode.** `transfer()` uses `ONCHAIN_UNCONSTRAINED` with no way to choose offchain delivery from the outside. Every contract wanting to offer offchain delivery must add a separate method (or delivery mode should become a parameter on standard methods). + +3. **"Duplicate method, swap delivery mode" pattern.** `transfer_offchain` is identical to `transfer` except for the delivery mode constant. This suggests delivery mode could be a parameter rather than requiring method duplication. + +4. **Self-delivery is manual friction (F-324).** The sender must explicitly call `offchain_receive` for their own change note. This is boilerplate every app must handle. Automatic self-delivery would eliminate this. + +5. **Sender has no feedback channel.** With direct offchain delivery (no escrow), the sender cannot know if the recipient received or processed the offchain message. This is inherent to the "fire and forget" model but worth documenting as a tradeoff. + +6. **Recipient address required upfront.** Because notes are encrypted for a specific recipient, there's no "send to anyone with the link" pattern possible. The link only works for the intended recipient. + +7. **URL-based delivery is feasible.** Estimated ~2,100 chars for a transfer link — within browser URL limits and QR code capacity. This validates URLs as a practical delivery channel for single-note transfers. diff --git a/scripts/deploy-subscription-fpc.ts b/scripts/deploy-subscription-fpc.ts index 2962258..23b273c 100644 --- a/scripts/deploy-subscription-fpc.ts +++ b/scripts/deploy-subscription-fpc.ts @@ -1,18 +1,13 @@ /** * Deploys the SubscriptionFPC contract to the local sandbox and updates local.json config. * - * Uses the artifact from @gregojuice/contracts and deploys via gregoswap's own SDK - * to avoid version mismatch issues. - * * Usage: node --experimental-transform-types scripts/deploy-subscription-fpc.ts */ import fs from 'fs'; import path from 'path'; -import { SubscriptionFPCContractArtifact } from '@gregojuice/contracts/artifacts/SubscriptionFPC'; +import { SubscriptionFPC } from '@gregojuice/contracts/subscription-fpc'; import { FunctionSelector } from '@aztec/stdlib/abi'; -import { Fr } from '@aztec/foundation/curves/bn254'; -import { Contract } from '@aztec/aztec.js/contracts'; import { ProofOfPasswordContractArtifact } from '../contracts/target/ProofOfPassword.ts'; import { AMMContractArtifact } from '../contracts/target/AMM.ts'; import { setupWallet, getOrCreateDeployer } from './utils.ts'; @@ -22,20 +17,9 @@ async function main() { const deployer = await getOrCreateDeployer(wallet, paymentMethod); console.log('Deploying SubscriptionFPC...'); - - // Generate a secret key for the FPC instance - const secretKey = Fr.random(); - const salt = Fr.random(); - - // Deploy using gregoswap's own SDK Contract.deploy - const { contract } = await Contract.deploy(wallet, SubscriptionFPCContractArtifact, [deployer]).send({ - from: deployer, - fee: { paymentMethod }, - contractAddressSalt: salt, - wait: { timeout: 120 }, - }); - - const fpcAddress = contract.address.toString(); + const { deployment, secretKey } = await SubscriptionFPC.deployWithKeys(wallet, deployer); + const receipt = await deployment.send({ fee: { paymentMethod } }); + const fpcAddress = receipt.contract.address.toString(); console.log('SubscriptionFPC deployed at:', fpcAddress); console.log('Secret key:', secretKey.toString()); diff --git a/src/services/contractService.ts b/src/services/contractService.ts index 237e84f..e9a5347 100644 --- a/src/services/contractService.ts +++ b/src/services/contractService.ts @@ -180,22 +180,22 @@ export async function registerDripContracts( } // Register subscription FPC if configured and not yet registered - if (!subFPC) { - throw new Error('No subscriptionFPC configured for this network'); - } - const subFPCMetadata = metadataResults[1]; - if (!subFPCMetadata.result.instance) { - const fpcAddress = AztecAddressClass.fromString(subFPC.address); - const secretKey = Fr.fromString(subFPC.secretKey); - const instance = await node.getContract(fpcAddress); - if (!instance) { - throw new Error(`Subscription FPC at ${subFPC.address} not found on-chain`); + if (subFPC) { + const subFPCMetadata = metadataResults[1]; + if (!subFPCMetadata?.result?.instance) { + const fpcAddress = AztecAddressClass.fromString(subFPC.address); + const secretKey = Fr.fromString(subFPC.secretKey); + const instance = await node.getContract(fpcAddress); + if (!instance) { + console.warn(`Subscription FPC at ${subFPC.address} not found on-chain, skipping`); + } else { + const { SubscriptionFPCContractArtifact } = await import('@gregojuice/contracts/artifacts/SubscriptionFPC'); + registrationBatch.push({ + name: 'registerContract', + args: [instance, SubscriptionFPCContractArtifact, secretKey], + }); + } } - const { SubscriptionFPCContractArtifact } = await import('@gregojuice/contracts/artifacts/SubscriptionFPC'); - registrationBatch.push({ - name: 'registerContract', - args: [instance, SubscriptionFPCContractArtifact, secretKey], - }); } // Only call batch if there are contracts to register @@ -515,7 +515,7 @@ export async function executeDrip( ): Promise { const subFPC = network.subscriptionFPC; if (!subFPC) { - throw new Error('No subscriptionFPC configured for this network'); + throw new Error('Drip requires subscriptionFPC which is not configured for this network. Use the Send tab to transfer tokens directly.'); } const call = await pop.methods.check_password_and_mint(password, recipient).getFunctionCall(); From 69348fd565ba12d98585ed23e4277306359dfd19 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Mon, 13 Apr 2026 10:34:03 +0000 Subject: [PATCH 14/24] final tweaks --- contracts/token/Nargo.toml | 8 +- contracts/token/src/main.nr | 21 + package.json | 27 +- scripts/deploy-subscription-fpc.ts | 90 +++- scripts/deploy.ts | 2 +- scripts/signup-fpc.ts | 71 +++ src/App.tsx | 19 +- src/components/claim/ClaimPage.tsx | 2 +- src/components/send/SendContainer.tsx | 14 +- src/contexts/contracts/ContractsContext.tsx | 14 +- src/contexts/contracts/reducer.ts | 2 +- src/contexts/send/SendContext.tsx | 3 +- src/services/contractService.ts | 88 ++- vite.config.ts | 2 +- yarn.lock | 564 ++++++++++---------- 15 files changed, 576 insertions(+), 351 deletions(-) create mode 100644 scripts/signup-fpc.ts diff --git a/contracts/token/Nargo.toml b/contracts/token/Nargo.toml index 8cd6e38..3ba9031 100644 --- a/contracts/token/Nargo.toml +++ b/contracts/token/Nargo.toml @@ -4,7 +4,7 @@ authors = [""] type = "contract" [dependencies] -aztec = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-aztecnr-rc.2", directory = "noir-projects/aztec-nr/aztec" } -uint_note = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-aztecnr-rc.2", directory = "noir-projects/aztec-nr/uint-note" } -compressed_string = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-aztecnr-rc.2", directory = "noir-projects/aztec-nr/compressed-string" } -balance_set = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-aztecnr-rc.2", directory = "noir-projects/aztec-nr/balance-set" } +aztec = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-nightly.20260410", directory = "noir-projects/aztec-nr/aztec" } +uint_note = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-nightly.20260410", directory = "noir-projects/aztec-nr/uint-note" } +compressed_string = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-nightly.20260410", directory = "noir-projects/aztec-nr/compressed-string" } +balance_set = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-nightly.20260410", directory = "noir-projects/aztec-nr/balance-set" } diff --git a/contracts/token/src/main.nr b/contracts/token/src/main.nr index 0ee715d..f4dcadb 100644 --- a/contracts/token/src/main.nr +++ b/contracts/token/src/main.nr @@ -265,6 +265,27 @@ pub contract Token { ); } + /// FPC-friendly variant of `transfer_offchain` that takes the sender explicitly, + /// allowing the call to be sponsored (msg_sender will be the FPC, not the user). + /// Authwitness ensures the user authorized this exact transfer. + #[authorize_once("from", "authwit_nonce")] + #[external("private")] + fn transfer_offchain_from( + from: AztecAddress, + to: AztecAddress, + amount: u128, + authwit_nonce: Field, + ) { + let change = self.internal.subtract_balance(from, amount, INITIAL_TRANSFER_CALL_MAX_NOTES); + self.storage.balances.at(from).add(change).deliver(MessageDelivery.OFFCHAIN); + self.storage.balances.at(to).add(amount).deliver(MessageDelivery.OFFCHAIN); + + self.emit(Transfer { from, to, amount }).deliver_to( + to, + MessageDelivery.OFFCHAIN, + ); + } + #[internal("private")] fn subtract_balance(account: AztecAddress, amount: u128, max_notes: u32) -> u128 { let subtracted = self.storage.balances.at(account).try_sub(amount, max_notes); diff --git a/package.json b/package.json index 3d1e683..bb8b9e2 100644 --- a/package.json +++ b/package.json @@ -27,20 +27,21 @@ "local-aztec:status": "node scripts/toggle-local-aztec.js status" }, "dependencies": { - "@aztec/accounts": "4.2.0-nightly.20260409", - "@aztec/aztec.js": "4.2.0-nightly.20260409", - "@aztec/constants": "4.2.0-nightly.20260409", - "@aztec/entrypoints": "4.2.0-nightly.20260409", - "@aztec/foundation": "4.2.0-nightly.20260409", - "@aztec/noir-contracts.js": "4.2.0-nightly.20260409", - "@aztec/protocol-contracts": "4.2.0-nightly.20260409", - "@aztec/pxe": "4.2.0-nightly.20260409", - "@aztec/stdlib": "4.2.0-nightly.20260409", - "@aztec/wallet-sdk": "4.2.0-nightly.20260409", + "@aztec/accounts": "4.2.0-nightly.20260410", + "@aztec/aztec.js": "4.2.0-nightly.20260410", + "@aztec/constants": "4.2.0-nightly.20260410", + "@aztec/entrypoints": "4.2.0-nightly.20260410", + "@aztec/ethereum": "4.2.0-nightly.20260410", + "@aztec/foundation": "4.2.0-nightly.20260410", + "@aztec/noir-contracts.js": "4.2.0-nightly.20260410", + "@aztec/protocol-contracts": "4.2.0-nightly.20260410", + "@aztec/pxe": "4.2.0-nightly.20260410", + "@aztec/stdlib": "4.2.0-nightly.20260410", + "@aztec/wallet-sdk": "4.2.0-nightly.20260410", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", - "@gregojuice/contracts": "portal:/mnt/user-data/martin/gregojuice/packages/contracts", - "@gregojuice/embedded-wallet": "portal:/mnt/user-data/martin/gregojuice/packages/embedded-wallet", + "@gregojuice/contracts": "0.0.12", + "@gregojuice/embedded-wallet": "0.0.12", "@mui/icons-material": "^6.3.1", "@mui/material": "^6.3.1", "@mui/styles": "^6.3.1", @@ -52,7 +53,7 @@ "zod": "^3.23.8" }, "devDependencies": { - "@aztec/wallets": "4.2.0-nightly.20260409", + "@aztec/wallets": "4.2.0-nightly.20260410", "@eslint/js": "^9.18.0", "@playwright/test": "1.49.0", "@types/buffer-json": "^2", diff --git a/scripts/deploy-subscription-fpc.ts b/scripts/deploy-subscription-fpc.ts index 23b273c..dec2ab9 100644 --- a/scripts/deploy-subscription-fpc.ts +++ b/scripts/deploy-subscription-fpc.ts @@ -8,17 +8,27 @@ import fs from 'fs'; import path from 'path'; import { SubscriptionFPC } from '@gregojuice/contracts/subscription-fpc'; import { FunctionSelector } from '@aztec/stdlib/abi'; +import { L1FeeJuicePortalManager } from '@aztec/aztec.js/ethereum'; +import { waitForL1ToL2MessageReady } from '@aztec/aztec.js/messaging'; +import { createExtendedL1Client } from '@aztec/ethereum/client'; +import { createLogger } from '@aztec/foundation/log'; +import { foundry } from 'viem/chains'; +import { Fr } from '@aztec/foundation/curves/bn254'; import { ProofOfPasswordContractArtifact } from '../contracts/target/ProofOfPassword.ts'; import { AMMContractArtifact } from '../contracts/target/AMM.ts'; +import { TokenContractArtifact } from '../contracts/target/Token.ts'; import { setupWallet, getOrCreateDeployer } from './utils.ts'; +// Well-known Anvil account #0 — used to sign the L1 bridge transaction on local sandbox +const ANVIL_KEY_0 = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'; + async function main() { - const { wallet, paymentMethod } = await setupWallet('http://localhost:8080', 'local'); + const { wallet, node, paymentMethod } = await setupWallet('http://localhost:8080', 'local'); const deployer = await getOrCreateDeployer(wallet, paymentMethod); console.log('Deploying SubscriptionFPC...'); const { deployment, secretKey } = await SubscriptionFPC.deployWithKeys(wallet, deployer); - const receipt = await deployment.send({ fee: { paymentMethod } }); + const receipt = await deployment.send({ from: deployer, fee: { paymentMethod } }); const fpcAddress = receipt.contract.address.toString(); console.log('SubscriptionFPC deployed at:', fpcAddress); console.log('Secret key:', secretKey.toString()); @@ -30,6 +40,12 @@ async function main() { const ammFn = AMMContractArtifact.functions.find(f => f.name === 'swap_tokens_for_exact_tokens_from'); const ammSelector = await FunctionSelector.fromNameAndParameters(ammFn!.name, ammFn!.parameters); + const transferOffchainFn = TokenContractArtifact.functions.find(f => f.name === 'transfer_offchain_from'); + const transferOffchainSelector = await FunctionSelector.fromNameAndParameters( + transferOffchainFn!.name, + transferOffchainFn!.parameters, + ); + // Update local.json const configPath = path.join(import.meta.dirname, '../src/config/networks/local.json'); const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); @@ -44,11 +60,81 @@ async function main() { [config.contracts.amm]: { [ammSelector.toString()]: 0, }, + [config.contracts.gregoCoin]: { + [transferOffchainSelector.toString()]: 0, + }, + [config.contracts.gregoCoinPremium]: { + [transferOffchainSelector.toString()]: 0, + }, }, }; fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); console.log(`\nUpdated ${configPath} with subscriptionFPC config.`); + + // Re-register the FPC contract with its secret key so PXE can compute tagging secrets + const { SubscriptionFPCContractArtifact: fpcArtifact } = await import('@gregojuice/contracts/artifacts/SubscriptionFPC'); + const fpcInstance = await node.getContract(receipt.contract.address); + if (!fpcInstance) throw new Error('FPC contract not found on-chain after deploy'); + await wallet.registerContract(fpcInstance, fpcArtifact, secretKey); + + // Start the L1 bridge early so the message can propagate while we do sign_up on L2. + // On local sandbox, the fee asset handler mints a fixed amount per call (1000 FJ). + // When mint=true, bridgeTokensPublic must match this exact amount. + const bridgeAmount: bigint = BigInt('1000000000000000000000'); // 1000 FJ + + console.log(`\nBridging ${bridgeAmount} wei of fee juice to FPC...`); + const l1Client = createExtendedL1Client(['http://localhost:8545'], ANVIL_KEY_0, foundry); + const portalManager = await L1FeeJuicePortalManager.new(node, l1Client, createLogger('bridge')); + const claim = await portalManager.bridgeTokensPublic(receipt.contract.address, bridgeAmount, true); + console.log('L1 bridge tx mined.'); + + // Sign up functions so users can subscribe. These L2 txs also advance the L2 chain, + // which helps the sequencer include the pending L1->L2 bridge message. + const { SubscriptionFPCContract } = await import('@gregojuice/contracts/artifacts/SubscriptionFPC'); + const fpc = SubscriptionFPCContract.at(receipt.contract.address, wallet); + + const maxUses = 100; + const maxFee = BigInt('1000000000000000000000'); // 1000 FJ + const maxUsers = 100; + + const popAddress = config.contracts.pop; + console.log(`\nSigning up PoP selector ${popSelector} at index 0...`); + await fpc.methods + .sign_up(popAddress, popSelector, 0, maxUses, maxFee, maxUsers) + .send({ from: deployer, fee: { paymentMethod } }); + console.log('PoP sign_up done!'); + + const ammAddress = config.contracts.amm; + console.log(`Signing up AMM selector ${ammSelector} at index 0...`); + await fpc.methods + .sign_up(ammAddress, ammSelector, 0, maxUses, maxFee, maxUsers) + .send({ from: deployer, fee: { paymentMethod } }); + console.log('AMM sign_up done!'); + + // Sign up transfer_offchain_from on both token contracts + for (const tokenKey of ['gregoCoin', 'gregoCoinPremium'] as const) { + const tokenAddress = config.contracts[tokenKey]; + console.log(`Signing up ${tokenKey}.transfer_offchain_from at index 0...`); + await fpc.methods + .sign_up(tokenAddress, transferOffchainSelector, 0, maxUses, maxFee, maxUsers) + .send({ from: deployer, fee: { paymentMethod } }); + console.log(`${tokenKey} sign_up done!`); + } + + // Wait for the L1->L2 bridge message and claim the FJ to credit the FPC's balance. + console.log('\nWaiting for L1->L2 message sync...'); + const messageHash = Fr.fromHexString(claim.messageHash); + await waitForL1ToL2MessageReady(node, messageHash, { timeoutSeconds: 120 }); + console.log('Message ready'); + + const { FeeJuiceContract } = await import('@aztec/aztec.js/protocol'); + const feeJuice = FeeJuiceContract.at(wallet); + console.log('Claiming fee juice on L2 for FPC...'); + await feeJuice.methods + .claim(receipt.contract.address, claim.claimAmount, claim.claimSecret, claim.messageLeafIndex) + .send({ from: deployer, fee: { paymentMethod } }); + console.log('FPC funded!'); } main().catch(err => { diff --git a/scripts/deploy.ts b/scripts/deploy.ts index b3407fb..bd0fdfc 100644 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -1,7 +1,7 @@ import fs from 'fs'; import path from 'path'; -import { TokenContract } from '@aztec/noir-contracts.js/Token'; +import { TokenContract } from '../contracts/target/Token.ts'; import { AMMContract } from '../contracts/target/AMM.ts'; import { AztecAddress } from '@aztec/stdlib/aztec-address'; import { Fr } from '@aztec/foundation/curves/bn254'; diff --git a/scripts/signup-fpc.ts b/scripts/signup-fpc.ts new file mode 100644 index 0000000..ae73504 --- /dev/null +++ b/scripts/signup-fpc.ts @@ -0,0 +1,71 @@ +/** + * Calls sign_up on the SubscriptionFPC for each configured function (PoP + AMM). + * Uses the same deployer wallet as deploy-subscription-fpc.ts. + * + * Usage: node --experimental-transform-types scripts/signup-fpc.ts + */ + +import fs from 'fs'; +import path from 'path'; +import { AztecAddress } from '@aztec/stdlib/aztec-address'; +import { FunctionSelector } from '@aztec/stdlib/abi'; +import { SubscriptionFPCContract } from '@gregojuice/contracts/artifacts/SubscriptionFPC'; +import { ProofOfPasswordContractArtifact } from '../contracts/target/ProofOfPassword.ts'; +import { AMMContractArtifact } from '../contracts/target/AMM.ts'; +import { setupWallet, getOrCreateDeployer } from './utils.ts'; + +async function main() { + const configPath = path.join(import.meta.dirname, '../src/config/networks/local.json'); + const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); + + const { wallet, node, paymentMethod } = await setupWallet('http://localhost:8080', 'local'); + const adminAddress = await getOrCreateDeployer(wallet, paymentMethod); + + const fpcAddress = AztecAddress.fromString(config.subscriptionFPC.address); + + // Register the FPC contract in this PXE so it can interact with it + const { SubscriptionFPCContractArtifact } = await import('@gregojuice/contracts/artifacts/SubscriptionFPC'); + const fpcInstance = await node.getContract(fpcAddress); + if (!fpcInstance) throw new Error('FPC contract not found on-chain'); + await wallet.registerContract(fpcInstance, SubscriptionFPCContractArtifact); + + const fpc = SubscriptionFPCContract.at(fpcAddress, wallet); + console.log('Admin:', adminAddress.toString()); + console.log('FPC:', fpcAddress.toString()); + + // sign_up params: generous for local dev + const maxUses = 100; + const maxFee = BigInt('1000000000000000000000'); // 1000 FJ in wei + const maxUsers = 100; + + // 1. Sign up PoP.check_password_and_mint + const popFn = ProofOfPasswordContractArtifact.functions.find(f => f.name === 'check_password_and_mint'); + const popSelector = await FunctionSelector.fromNameAndParameters(popFn!.name, popFn!.parameters); + const popAddress = AztecAddress.fromString(config.contracts.pop); + const popConfigIndex = config.subscriptionFPC.functions[config.contracts.pop][popSelector.toString()]; + + console.log(`\nSigning up PoP (${popAddress}) selector ${popSelector} at index ${popConfigIndex}...`); + await fpc.methods + .sign_up(popAddress, popSelector, popConfigIndex, maxUses, maxFee, maxUsers) + .send({ from: adminAddress, fee: { paymentMethod } }); + console.log('PoP sign_up done!'); + + // 2. Sign up AMM.swap_tokens_for_exact_tokens_from + const ammFn = AMMContractArtifact.functions.find(f => f.name === 'swap_tokens_for_exact_tokens_from'); + const ammSelector = await FunctionSelector.fromNameAndParameters(ammFn!.name, ammFn!.parameters); + const ammAddress = AztecAddress.fromString(config.contracts.amm); + const ammConfigIndex = config.subscriptionFPC.functions[config.contracts.amm][ammSelector.toString()]; + + console.log(`\nSigning up AMM (${ammAddress}) selector ${ammSelector} at index ${ammConfigIndex}...`); + await fpc.methods + .sign_up(ammAddress, ammSelector, ammConfigIndex, maxUses, maxFee, maxUsers) + .send({ from: adminAddress, fee: { paymentMethod } }); + console.log('AMM sign_up done!'); + + console.log('\nAll functions signed up successfully!'); +} + +main().catch(err => { + console.error(err); + process.exit(1); +}); diff --git a/src/App.tsx b/src/App.tsx index 88054b6..0393b46 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { ThemeProvider, CssBaseline, Container, Box, Typography, Tabs, Tab } from '@mui/material'; +import { ThemeProvider, CssBaseline, Container, Box, Typography, Tabs, Tab, Snackbar } from '@mui/material'; import { theme } from './theme'; import { GregoSwapLogo } from './components/GregoSwapLogo'; import { WalletChip } from './components/WalletChip'; @@ -17,17 +17,20 @@ import type { AztecAddress } from '@aztec/aztec.js/addresses'; export function App() { const [activeTab, setActiveTab] = useState(0); + const [addressCopied, setAddressCopied] = useState(false); const { disconnectWallet, setCurrentAddress, currentAddress, error: walletError, isLoading: walletLoading } = useWallet(); const { isOnboardingModalOpen, startOnboarding, resetOnboarding, status: onboardingStatus } = useOnboarding(); const isOnboarded = onboardingStatus === 'completed'; - const handleWalletClick = () => { - // If already onboarded, start a new onboarding flow to change wallet + const handleWalletClick = async () => { + // If connected, copy the address. Otherwise start onboarding. if (isOnboarded && currentAddress) { - resetOnboarding(); + await navigator.clipboard.writeText(currentAddress.toString()); + setAddressCopied(true); + return; } - startOnboarding(); // Start onboarding when clicked from wallet chip + startOnboarding(); }; const handleDisconnect = async () => { @@ -106,6 +109,12 @@ export function App() { onClick={handleWalletClick} onDisconnect={handleDisconnect} /> + setAddressCopied(false)} + message="Address copied!" + /> {/* Header */} diff --git a/src/components/claim/ClaimPage.tsx b/src/components/claim/ClaimPage.tsx index 31e6bb7..5cf2504 100644 --- a/src/components/claim/ClaimPage.tsx +++ b/src/components/claim/ClaimPage.tsx @@ -63,7 +63,7 @@ export function ClaimPage() { await claimOffchainTransfer(tokenKey, { ciphertext: data.payload.map((s: string) => Fr.fromString(s)), recipient: AztecAddress.fromString(data.recipient), - tx_hash: data.txHash, + tx_hash: Fr.fromString(data.txHash), anchor_block_timestamp: BigInt(data.anchorBlockTimestamp), }); diff --git a/src/components/send/SendContainer.tsx b/src/components/send/SendContainer.tsx index 0a09840..19cec0c 100644 --- a/src/components/send/SendContainer.tsx +++ b/src/components/send/SendContainer.tsx @@ -10,15 +10,15 @@ import { useEffect, useState } from 'react'; export function SendContainer() { const { phase, error, generatedLink, token, amount, recipientAddress, dismissError, reset } = useSend(); - const { currentAddress, isUsingEmbeddedWallet } = useWallet(); + const { currentAddress } = useWallet(); const { fetchBalances } = useContracts(); const [balances, setBalances] = useState<{ gc: bigint | null; gcp: bigint | null }>({ gc: null, gcp: null }); useEffect(() => { - if (currentAddress && !isUsingEmbeddedWallet) { + if (currentAddress) { fetchBalances().then(([gc, gcp]) => setBalances({ gc, gcp })); } - }, [currentAddress, isUsingEmbeddedWallet, fetchBalances]); + }, [currentAddress, fetchBalances]); useEffect(() => { if (phase === 'link_ready' && currentAddress) { @@ -26,14 +26,6 @@ export function SendContainer() { } }, [phase, currentAddress, fetchBalances]); - if (isUsingEmbeddedWallet) { - return ( - - Connect an external wallet to send tokens. - - ); - } - return ( {phase === 'link_ready' && generatedLink ? ( diff --git a/src/contexts/contracts/ContractsContext.tsx b/src/contexts/contracts/ContractsContext.tsx index 2eee315..8e53b97 100644 --- a/src/contexts/contracts/ContractsContext.tsx +++ b/src/contexts/contracts/ContractsContext.tsx @@ -5,8 +5,10 @@ import { createContext, useContext, useEffect, type ReactNode, useCallback } from 'react'; import type { AztecAddress } from '@aztec/aztec.js/addresses'; +import type { Fr } from '@aztec/foundation/curves/bn254'; import type { TxReceipt } from '@aztec/stdlib/tx'; import type { AMMContract } from '../../../contracts/target/AMM'; +import type { OffchainMessage } from '../../services/contractService'; import { useWallet } from '../wallet'; import { useNetwork } from '../network'; import * as contractService from '../../services/contractService'; @@ -27,8 +29,8 @@ interface ContractsContextType { fetchBalances: () => Promise<[bigint, bigint]>; simulateOnboardingQueries: () => Promise<[number, bigint, bigint]>; drip: (password: string, recipient: AztecAddress) => Promise; - sendOffchain: (tokenKey: 'gregoCoin' | 'gregoCoinPremium', recipient: AztecAddress, amount: bigint) => Promise<{ receipt: TxReceipt; offchainMessages: any[] }>; - claimOffchainTransfer: (tokenKey: 'gregoCoin' | 'gregoCoinPremium', message: { ciphertext: any[]; recipient: AztecAddress; tx_hash: string; anchor_block_timestamp: bigint }) => Promise; + sendOffchain: (tokenKey: 'gregoCoin' | 'gregoCoinPremium', recipient: AztecAddress, amount: bigint) => Promise<{ receipt: TxReceipt; offchainMessages: OffchainMessage[] }>; + claimOffchainTransfer: (tokenKey: 'gregoCoin' | 'gregoCoinPremium', message: { ciphertext: Fr[]; recipient: AztecAddress; tx_hash: Fr; anchor_block_timestamp: bigint }) => Promise; } const ContractsContext = createContext(undefined); @@ -231,6 +233,8 @@ export function ContractsProvider({ children }: ContractsProviderProps) { throw new Error('Contracts not initialized'); } return contractService.executeTransferOffchain( + wallet, + activeNetwork, { gregoCoin: state.contracts.gregoCoin, gregoCoinPremium: state.contracts.gregoCoinPremium, @@ -241,18 +245,18 @@ export function ContractsProvider({ children }: ContractsProviderProps) { recipient, amount, ); - }, [wallet, currentAddress, state.contracts]); + }, [wallet, activeNetwork, currentAddress, state.contracts]); // Claim an offchain transfer via offchain_receive const claimOffchainTransfer = useCallback(async ( tokenKey: 'gregoCoin' | 'gregoCoinPremium', - message: { ciphertext: any[]; recipient: AztecAddress; tx_hash: string; anchor_block_timestamp: bigint }, + message: { ciphertext: Fr[]; recipient: AztecAddress; tx_hash: Fr; anchor_block_timestamp: bigint }, ) => { if (!wallet || !currentAddress || !state.contracts.gregoCoin || !state.contracts.gregoCoinPremium) { throw new Error('Contracts not initialized'); } const token = tokenKey === 'gregoCoin' ? state.contracts.gregoCoin : state.contracts.gregoCoinPremium; - await (token.methods as any) + await token.methods .offchain_receive([message]) .simulate({ from: currentAddress }); }, [wallet, currentAddress, state.contracts]); diff --git a/src/contexts/contracts/reducer.ts b/src/contexts/contracts/reducer.ts index 2b44b62..5396aea 100644 --- a/src/contexts/contracts/reducer.ts +++ b/src/contexts/contracts/reducer.ts @@ -3,7 +3,7 @@ * Manages contract instances and registration state */ -import type { TokenContract } from '@aztec/noir-contracts.js/Token'; +import type { TokenContract } from '../../../contracts/target/Token'; import type { AMMContract } from '../../../contracts/target/AMM'; import type { ProofOfPasswordContract } from '../../../contracts/target/ProofOfPassword'; import { createReducerHook, type ActionsFrom } from '../utils'; diff --git a/src/contexts/send/SendContext.tsx b/src/contexts/send/SendContext.tsx index 2b68bf8..93bfd75 100644 --- a/src/contexts/send/SendContext.tsx +++ b/src/contexts/send/SendContext.tsx @@ -43,7 +43,7 @@ interface SendProviderProps { export function SendProvider({ children }: SendProviderProps) { const [state, actions] = useSendReducer(); const { sendOffchain, isLoadingContracts } = useContracts(); - const { currentAddress, isUsingEmbeddedWallet } = useWallet(); + const { currentAddress } = useWallet(); const { activeNetwork } = useNetwork(); const canSend = @@ -51,7 +51,6 @@ export function SendProvider({ children }: SendProviderProps) { parseFloat(state.amount) > 0 && !!state.recipientAddress && !isLoadingContracts && - !isUsingEmbeddedWallet && !!currentAddress; const executeSend = useCallback(async () => { diff --git a/src/services/contractService.ts b/src/services/contractService.ts index e9a5347..593c2ef 100644 --- a/src/services/contractService.ts +++ b/src/services/contractService.ts @@ -12,7 +12,7 @@ import { FunctionSelector } from '@aztec/aztec.js/abi'; import { BatchCall, getContractInstanceFromInstantiationParams } from '@aztec/aztec.js/contracts'; import { poseidon2Hash } from '@aztec/foundation/crypto/poseidon'; import type { TxReceipt } from '@aztec/stdlib/tx'; -import type { TokenContract } from '@aztec/noir-contracts.js/Token'; +import type { TokenContract } from '../../contracts/target/Token'; import type { AMMContract } from '../../contracts/target/AMM'; import type { ProofOfPasswordContract } from '../../contracts/target/ProofOfPassword'; import { BigDecimal } from '../utils/bigDecimal'; @@ -53,7 +53,7 @@ export async function registerSwapContracts( const contractSalt = Fr.fromString(network.contracts.salt); // Import contract artifacts - const { TokenContract, TokenContractArtifact } = await import('@aztec/noir-contracts.js/Token'); + const { TokenContract, TokenContractArtifact } = await import('../../contracts/target/Token'); const { AMMContract, AMMContractArtifact } = await import('../../contracts/target/AMM'); // Determine subscription FPC for sponsored swaps @@ -180,22 +180,22 @@ export async function registerDripContracts( } // Register subscription FPC if configured and not yet registered - if (subFPC) { - const subFPCMetadata = metadataResults[1]; - if (!subFPCMetadata?.result?.instance) { - const fpcAddress = AztecAddressClass.fromString(subFPC.address); - const secretKey = Fr.fromString(subFPC.secretKey); - const instance = await node.getContract(fpcAddress); - if (!instance) { - console.warn(`Subscription FPC at ${subFPC.address} not found on-chain, skipping`); - } else { - const { SubscriptionFPCContractArtifact } = await import('@gregojuice/contracts/artifacts/SubscriptionFPC'); - registrationBatch.push({ - name: 'registerContract', - args: [instance, SubscriptionFPCContractArtifact, secretKey], - }); - } + if (!subFPC) { + throw new Error('No subscriptionFPC configured for this network'); + } + const subFPCMetadata = metadataResults[1]; + if (!subFPCMetadata.result.instance) { + const fpcAddress = AztecAddressClass.fromString(subFPC.address); + const secretKey = Fr.fromString(subFPC.secretKey); + const instance = await node.getContract(fpcAddress); + if (!instance) { + throw new Error(`Subscription FPC at ${subFPC.address} not found on-chain`); } + const { SubscriptionFPCContractArtifact } = await import('@gregojuice/contracts/artifacts/SubscriptionFPC'); + registrationBatch.push({ + name: 'registerContract', + args: [instance, SubscriptionFPCContractArtifact, secretKey], + }); } // Only call batch if there are contracts to register @@ -515,7 +515,7 @@ export async function executeDrip( ): Promise { const subFPC = network.subscriptionFPC; if (!subFPC) { - throw new Error('Drip requires subscriptionFPC which is not configured for this network. Use the Send tab to transfer tokens directly.'); + throw new Error('No subscriptionFPC configured for this network'); } const call = await pop.methods.check_password_and_mint(password, recipient).getFunctionCall(); @@ -557,25 +557,63 @@ export interface OffchainMessage { * change note, and returns the recipient's offchain messages for link encoding. */ export async function executeTransferOffchain( + wallet: Wallet, + network: NetworkConfig, contracts: SwapContracts, tokenKey: 'gregoCoin' | 'gregoCoinPremium', fromAddress: AztecAddress, recipient: AztecAddress, amount: bigint, ): Promise<{ receipt: TxReceipt; offchainMessages: OffchainMessage[] }> { + const subFPC = network.subscriptionFPC; + if (!subFPC) { + throw new Error('No subscriptionFPC configured for this network'); + } + const token = contracts[tokenKey]; - // 1. Send the offchain transfer transaction - const { receipt, offchainMessages } = await (token.methods as any) - .transfer_offchain(recipient, amount) - .send({ from: fromAddress }); + // Build the FPC-friendly call (transfer_offchain_from takes the sender explicitly + + // an authwit_nonce so the wallet can authorize the FPC to dispatch on the user's behalf) + const authwitNonce = Fr.random(); + const call = await token.methods + .transfer_offchain_from(fromAddress, recipient, amount, authwitNonce) + .getFunctionCall(); + + const configIndex = subFPC.functions[token.address.toString()]?.[call.selector.toString()]; + if (configIndex == null) { + throw new Error( + `No subscription config found for token ${token.address.toString()} selector ${call.selector.toString()}`, + ); + } + + const fpcAddress = AztecAddressClass.fromString(subFPC.address); + const { SubscriptionFPCContract } = await import('@gregojuice/contracts/artifacts/SubscriptionFPC'); + const { SubscriptionFPC } = await import('@gregojuice/contracts/subscription-fpc'); + const rawFPC = SubscriptionFPCContract.at(fpcAddress, wallet); + const fpc = new SubscriptionFPC(rawFPC); + + // Create an authwitness so the FPC can call transfer_offchain_from on the user's behalf. + // The caller from the token contract's perspective is the FPC. + const authWitness = await wallet.createAuthWit(fromAddress, { caller: fpcAddress, call }); + + const subscribed = hasSubscription(subFPC.address, configIndex, fromAddress.toString()); + + let txResult: { receipt: TxReceipt; offchainMessages: OffchainMessage[] }; + if (subscribed) { + txResult = await fpc.helpers.sponsor({ call, configIndex, userAddress: fromAddress, authWitnesses: [authWitness] }); + } else { + txResult = await fpc.helpers.subscribe({ call, configIndex, userAddress: fromAddress, authWitnesses: [authWitness] }); + markSubscribed(subFPC.address, configIndex, fromAddress.toString()); + } + + const { receipt, offchainMessages } = txResult; - // 2. Self-deliver sender's change note (manual until F-324 lands) + // Self-deliver sender's change note (manual until F-324 lands) const senderMessages = offchainMessages.filter( (msg: OffchainMessage) => msg.recipient.equals(fromAddress), ); if (senderMessages.length > 0) { - await (token.methods as any) + await token.methods .offchain_receive( senderMessages.map((msg: OffchainMessage) => ({ ciphertext: msg.payload, @@ -587,7 +625,7 @@ export async function executeTransferOffchain( .simulate({ from: fromAddress }); } - // 3. Filter and return recipient's messages for link encoding + // Filter and return recipient's messages for link encoding const recipientMessages = offchainMessages.filter( (msg: OffchainMessage) => msg.recipient.equals(recipient), ); diff --git a/vite.config.ts b/vite.config.ts index 91f584a..3cbd1a8 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -117,7 +117,7 @@ export default defineConfig(({ mode }) => { chunkSizeValidator([ { pattern: /assets\/index-.*\.js$/, - maxSizeKB: 1600, + maxSizeKB: 1700, description: 'Main entrypoint, hard limit', }, { diff --git a/yarn.lock b/yarn.lock index c6bc5f5..3bfbd15 100644 --- a/yarn.lock +++ b/yarn.lock @@ -645,66 +645,66 @@ __metadata: languageName: node linkType: hard -"@aztec/accounts@npm:4.2.0-nightly.20260409": - version: 4.2.0-nightly.20260409 - resolution: "@aztec/accounts@npm:4.2.0-nightly.20260409" - dependencies: - "@aztec/aztec.js": "npm:4.2.0-nightly.20260409" - "@aztec/entrypoints": "npm:4.2.0-nightly.20260409" - "@aztec/ethereum": "npm:4.2.0-nightly.20260409" - "@aztec/foundation": "npm:4.2.0-nightly.20260409" - "@aztec/stdlib": "npm:4.2.0-nightly.20260409" +"@aztec/accounts@npm:4.2.0-nightly.20260410": + version: 4.2.0-nightly.20260410 + resolution: "@aztec/accounts@npm:4.2.0-nightly.20260410" + dependencies: + "@aztec/aztec.js": "npm:4.2.0-nightly.20260410" + "@aztec/entrypoints": "npm:4.2.0-nightly.20260410" + "@aztec/ethereum": "npm:4.2.0-nightly.20260410" + "@aztec/foundation": "npm:4.2.0-nightly.20260410" + "@aztec/stdlib": "npm:4.2.0-nightly.20260410" tslib: "npm:^2.4.0" - checksum: 10c0/f3b7abde1f11a5e869d06d3cf3c5686b92eea2b7aaae64f5790f77219d38920f5d3c97b018c7bab28916c87edb34208b2907af76d856faff8c7019189236885c + checksum: 10c0/1d9bc83c11068ef6cd419220b2a6a5e182fb355ea73eb160edc296ea69584f190ca4744750f24ec2d3e6784b9279d4c7fe542858a3993c29c4e9726745fc26a1 languageName: node linkType: hard -"@aztec/aztec.js@npm:4.2.0-nightly.20260409": - version: 4.2.0-nightly.20260409 - resolution: "@aztec/aztec.js@npm:4.2.0-nightly.20260409" +"@aztec/aztec.js@npm:4.2.0-nightly.20260410, @aztec/aztec.js@npm:v4.2.0-nightly.20260410": + version: 4.2.0-nightly.20260410 + resolution: "@aztec/aztec.js@npm:4.2.0-nightly.20260410" dependencies: - "@aztec/constants": "npm:4.2.0-nightly.20260409" - "@aztec/entrypoints": "npm:4.2.0-nightly.20260409" - "@aztec/ethereum": "npm:4.2.0-nightly.20260409" - "@aztec/foundation": "npm:4.2.0-nightly.20260409" - "@aztec/l1-artifacts": "npm:4.2.0-nightly.20260409" - "@aztec/protocol-contracts": "npm:4.2.0-nightly.20260409" - "@aztec/stdlib": "npm:4.2.0-nightly.20260409" + "@aztec/constants": "npm:4.2.0-nightly.20260410" + "@aztec/entrypoints": "npm:4.2.0-nightly.20260410" + "@aztec/ethereum": "npm:4.2.0-nightly.20260410" + "@aztec/foundation": "npm:4.2.0-nightly.20260410" + "@aztec/l1-artifacts": "npm:4.2.0-nightly.20260410" + "@aztec/protocol-contracts": "npm:4.2.0-nightly.20260410" + "@aztec/stdlib": "npm:4.2.0-nightly.20260410" axios: "npm:^1.13.5" tslib: "npm:^2.4.0" viem: "npm:@aztec/viem@2.38.2" zod: "npm:^3.23.8" - checksum: 10c0/e45b348e7f71979e2fd8dd33991b28c5f127239c2b7571d5616e6ef34d7c49412e5bfd2b460ae154f9e2664fa4e61838ad94d0c263b852b4f899ddca0e40f945 + checksum: 10c0/37470f5946fa8008dc16ed89edb0735d931c103f0aed1cde5790dd9125ae92f440945648e559d5275a1dc4adeb70c8a67676994ca3f9bad6a1429cbaa8a91ab6 languageName: node linkType: hard -"@aztec/bb-prover@npm:4.2.0-nightly.20260409": - version: 4.2.0-nightly.20260409 - resolution: "@aztec/bb-prover@npm:4.2.0-nightly.20260409" +"@aztec/bb-prover@npm:4.2.0-nightly.20260410": + version: 4.2.0-nightly.20260410 + resolution: "@aztec/bb-prover@npm:4.2.0-nightly.20260410" dependencies: - "@aztec/bb.js": "npm:4.2.0-nightly.20260409" - "@aztec/constants": "npm:4.2.0-nightly.20260409" - "@aztec/foundation": "npm:4.2.0-nightly.20260409" - "@aztec/noir-noirc_abi": "npm:4.2.0-nightly.20260409" - "@aztec/noir-protocol-circuits-types": "npm:4.2.0-nightly.20260409" - "@aztec/noir-types": "npm:4.2.0-nightly.20260409" - "@aztec/simulator": "npm:4.2.0-nightly.20260409" - "@aztec/stdlib": "npm:4.2.0-nightly.20260409" - "@aztec/telemetry-client": "npm:4.2.0-nightly.20260409" - "@aztec/world-state": "npm:4.2.0-nightly.20260409" + "@aztec/bb.js": "npm:4.2.0-nightly.20260410" + "@aztec/constants": "npm:4.2.0-nightly.20260410" + "@aztec/foundation": "npm:4.2.0-nightly.20260410" + "@aztec/noir-noirc_abi": "npm:4.2.0-nightly.20260410" + "@aztec/noir-protocol-circuits-types": "npm:4.2.0-nightly.20260410" + "@aztec/noir-types": "npm:4.2.0-nightly.20260410" + "@aztec/simulator": "npm:4.2.0-nightly.20260410" + "@aztec/stdlib": "npm:4.2.0-nightly.20260410" + "@aztec/telemetry-client": "npm:4.2.0-nightly.20260410" + "@aztec/world-state": "npm:4.2.0-nightly.20260410" commander: "npm:^12.1.0" pako: "npm:^2.1.0" source-map-support: "npm:^0.5.21" tslib: "npm:^2.4.0" bin: bb-cli: dest/bb/index.js - checksum: 10c0/a7c8add943a9c182095baba743cd17a98da9fe4dcde289ac9bb82534d181be31899b25873d1c905958873166431811af5226e879bde463f91e8f2d4e92b09c4b + checksum: 10c0/e1a1b25e57f6ce695582de95a7b492c07e2576d76de374b9f72eb8e52c4760beb10aaf1700f0af546c21e5e4f266215c7614d90b2954df2a520b62bc409da793 languageName: node linkType: hard -"@aztec/bb.js@npm:4.2.0-nightly.20260409": - version: 4.2.0-nightly.20260409 - resolution: "@aztec/bb.js@npm:4.2.0-nightly.20260409" +"@aztec/bb.js@npm:4.2.0-nightly.20260410": + version: 4.2.0-nightly.20260410 + resolution: "@aztec/bb.js@npm:4.2.0-nightly.20260410" dependencies: comlink: "npm:^4.4.1" commander: "npm:^12.1.0" @@ -714,65 +714,65 @@ __metadata: tslib: "npm:^2.4.0" bin: bb: dest/node/bin/index.js - checksum: 10c0/7a3b181de1343b212dac8ed5dfe799b1ca0798b13160e49e88d4be5ee05d1e6761a3b2130b25afd975307494589ceae36b5eab1c3ff92ba1a4367406d9401e3b + checksum: 10c0/b776a637cebf3f1d6876d30155aab8da1eb3285b8c1d0cb1f30ffa862ab6f95bfaeb365732638d46e6ef56d6aaffb4a3499b26d210d30b0e3a9b643e28a36824 languageName: node linkType: hard -"@aztec/blob-lib@npm:4.2.0-nightly.20260409": - version: 4.2.0-nightly.20260409 - resolution: "@aztec/blob-lib@npm:4.2.0-nightly.20260409" +"@aztec/blob-lib@npm:4.2.0-nightly.20260410": + version: 4.2.0-nightly.20260410 + resolution: "@aztec/blob-lib@npm:4.2.0-nightly.20260410" dependencies: - "@aztec/constants": "npm:4.2.0-nightly.20260409" - "@aztec/foundation": "npm:4.2.0-nightly.20260409" + "@aztec/constants": "npm:4.2.0-nightly.20260410" + "@aztec/foundation": "npm:4.2.0-nightly.20260410" "@crate-crypto/node-eth-kzg": "npm:^0.10.0" tslib: "npm:^2.4.0" - checksum: 10c0/f712c6d82c5895ca24d453911ee75fffa221cd6044ccea632b5396a2ac308640699dde77a34190b18190200ade3b5c0ffbf91e5cb9e0e1e8660fea4b97c0cebd + checksum: 10c0/c95db2977395ae5fd147b9b8acf4182f53387368b11d9f41dc454aa74087db01bbea1cf0dffe90a16a5884a61fa35f9ee5fa734ea4d1c8df3dbd057e37fca75e languageName: node linkType: hard -"@aztec/builder@npm:4.2.0-nightly.20260409": - version: 4.2.0-nightly.20260409 - resolution: "@aztec/builder@npm:4.2.0-nightly.20260409" +"@aztec/builder@npm:4.2.0-nightly.20260410": + version: 4.2.0-nightly.20260410 + resolution: "@aztec/builder@npm:4.2.0-nightly.20260410" dependencies: - "@aztec/foundation": "npm:4.2.0-nightly.20260409" - "@aztec/stdlib": "npm:4.2.0-nightly.20260409" + "@aztec/foundation": "npm:4.2.0-nightly.20260410" + "@aztec/stdlib": "npm:4.2.0-nightly.20260410" commander: "npm:^12.1.0" - checksum: 10c0/44e067625daae1a377125801020535c033ae8d3fcfa7e6032f0588415a68a299814a87bba29dbc3ebe05e443843da2b2cba7c5ae1c538a4370c3b2937f8909d5 + checksum: 10c0/53547a204b1ea3bfe9694e9017b780b1361c92cced68abedc3410da0b28eab5dead846e317beca6b75d6c8a57e6d5034928ca357eb121763c18d9ba0f18afcab languageName: node linkType: hard -"@aztec/constants@npm:4.2.0-nightly.20260409": - version: 4.2.0-nightly.20260409 - resolution: "@aztec/constants@npm:4.2.0-nightly.20260409" +"@aztec/constants@npm:4.2.0-nightly.20260410": + version: 4.2.0-nightly.20260410 + resolution: "@aztec/constants@npm:4.2.0-nightly.20260410" dependencies: - "@aztec/foundation": "npm:4.2.0-nightly.20260409" + "@aztec/foundation": "npm:4.2.0-nightly.20260410" tslib: "npm:^2.4.0" - checksum: 10c0/aed35931ae458f098c1f68089a5a0d5db5c958ee8496e8d83f294ed3191bd5a0a2a3fca7da24fd819281071e0cab9854911ebdce247e2989f8c02fd9b46272ca + checksum: 10c0/1904dc178c36572cbd599f6ccd10e35af4f90d15aa4e2ae273c3b0d1938135ef62dcecf82a8ce3f19656ae9608c3ffd1806c7b2d930e08dec8173eb177cb39a7 languageName: node linkType: hard -"@aztec/entrypoints@npm:4.2.0-nightly.20260409": - version: 4.2.0-nightly.20260409 - resolution: "@aztec/entrypoints@npm:4.2.0-nightly.20260409" +"@aztec/entrypoints@npm:4.2.0-nightly.20260410, @aztec/entrypoints@npm:v4.2.0-nightly.20260410": + version: 4.2.0-nightly.20260410 + resolution: "@aztec/entrypoints@npm:4.2.0-nightly.20260410" dependencies: - "@aztec/constants": "npm:4.2.0-nightly.20260409" - "@aztec/foundation": "npm:4.2.0-nightly.20260409" - "@aztec/protocol-contracts": "npm:4.2.0-nightly.20260409" - "@aztec/stdlib": "npm:4.2.0-nightly.20260409" + "@aztec/constants": "npm:4.2.0-nightly.20260410" + "@aztec/foundation": "npm:4.2.0-nightly.20260410" + "@aztec/protocol-contracts": "npm:4.2.0-nightly.20260410" + "@aztec/stdlib": "npm:4.2.0-nightly.20260410" tslib: "npm:^2.4.0" zod: "npm:^3.23.8" - checksum: 10c0/bce90b402990dcf4d1abd4656ac5f15ff43151fe54dfffcf165101cf1eee176ac935bc1ebb29b95d804f28bef0b82f6b0938eed957bb2578b928cb596079093d + checksum: 10c0/a0837bc3815a00cbaa16cbd3bbceb0a5b484fb14af62529f813e6141bd3aeb8a8147493f66b9603d80800643882ca39570c3e4a21577d55456d862ab87e8f27e languageName: node linkType: hard -"@aztec/ethereum@npm:4.2.0-nightly.20260409": - version: 4.2.0-nightly.20260409 - resolution: "@aztec/ethereum@npm:4.2.0-nightly.20260409" +"@aztec/ethereum@npm:4.2.0-nightly.20260410": + version: 4.2.0-nightly.20260410 + resolution: "@aztec/ethereum@npm:4.2.0-nightly.20260410" dependencies: - "@aztec/blob-lib": "npm:4.2.0-nightly.20260409" - "@aztec/constants": "npm:4.2.0-nightly.20260409" - "@aztec/foundation": "npm:4.2.0-nightly.20260409" - "@aztec/l1-artifacts": "npm:4.2.0-nightly.20260409" + "@aztec/blob-lib": "npm:4.2.0-nightly.20260410" + "@aztec/constants": "npm:4.2.0-nightly.20260410" + "@aztec/foundation": "npm:4.2.0-nightly.20260410" + "@aztec/l1-artifacts": "npm:4.2.0-nightly.20260410" "@viem/anvil": "npm:^0.0.10" dotenv: "npm:^16.0.3" lodash.chunk: "npm:^4.2.0" @@ -780,15 +780,15 @@ __metadata: tslib: "npm:^2.4.0" viem: "npm:@aztec/viem@2.38.2" zod: "npm:^3.23.8" - checksum: 10c0/9d5e344877d62778b4150156b84626d3a241ba4d4cd66bb94aaed100c52f9ef458f54dee7af81547a80e597bfd7364a485d4590bf3e7be453dee89762657adbb + checksum: 10c0/483a7065ed9b5934afc90bde20c2b143c22496598dd9c7ab8ecd78490bb5b8b8f1eb12f05a97d906be923324df7cb00179cc4f957a3957dfed615808d57ed9af languageName: node linkType: hard -"@aztec/foundation@npm:4.2.0-nightly.20260409": - version: 4.2.0-nightly.20260409 - resolution: "@aztec/foundation@npm:4.2.0-nightly.20260409" +"@aztec/foundation@npm:4.2.0-nightly.20260410, @aztec/foundation@npm:v4.2.0-nightly.20260410": + version: 4.2.0-nightly.20260410 + resolution: "@aztec/foundation@npm:4.2.0-nightly.20260410" dependencies: - "@aztec/bb.js": "npm:4.2.0-nightly.20260409" + "@aztec/bb.js": "npm:4.2.0-nightly.20260410" "@koa/cors": "npm:^5.0.0" "@noble/curves": "npm:=1.7.0" "@noble/hashes": "npm:^1.6.1" @@ -810,169 +810,169 @@ __metadata: sha3: "npm:^2.1.4" undici: "npm:^5.28.5" zod: "npm:^3.23.8" - checksum: 10c0/26878b2208b9ef11f9d4df4e077e1f4dafff044842295485e997c81d49b866a09e32b97f408b7fcb4154e0c3d488a499cc5f0713ecbe9c7c5c69de09ede7ff5d + checksum: 10c0/2ba2754dc52fe8532cf63ddc44d8de6093a7e0ae562d627b3fd8e4cb1948cab0e50224e95a8587a7ffa37732df7b57f8291108bb4dce9b0cc87b35cacac1a4c3 languageName: node linkType: hard -"@aztec/key-store@npm:4.2.0-nightly.20260409": - version: 4.2.0-nightly.20260409 - resolution: "@aztec/key-store@npm:4.2.0-nightly.20260409" +"@aztec/key-store@npm:4.2.0-nightly.20260410": + version: 4.2.0-nightly.20260410 + resolution: "@aztec/key-store@npm:4.2.0-nightly.20260410" dependencies: - "@aztec/constants": "npm:4.2.0-nightly.20260409" - "@aztec/foundation": "npm:4.2.0-nightly.20260409" - "@aztec/kv-store": "npm:4.2.0-nightly.20260409" - "@aztec/stdlib": "npm:4.2.0-nightly.20260409" + "@aztec/constants": "npm:4.2.0-nightly.20260410" + "@aztec/foundation": "npm:4.2.0-nightly.20260410" + "@aztec/kv-store": "npm:4.2.0-nightly.20260410" + "@aztec/stdlib": "npm:4.2.0-nightly.20260410" tslib: "npm:^2.4.0" - checksum: 10c0/f14bfe99146ce6dbebdb6f755d5ea4ac68585739437f43aec4351d0fcbc4af30de9a97062c71a088e3b6c62676600f3ca70c6f2d14d5254c95a3264371a32dfc + checksum: 10c0/5f85ea0cd6a1b36ffca50afc470909eadecbdc8693410145813d24ec90da32d86553510a1781a2d67f1b770f0af69fe3bdb40788c12322316b34639ff6dcd93b languageName: node linkType: hard -"@aztec/kv-store@npm:4.2.0-nightly.20260409": - version: 4.2.0-nightly.20260409 - resolution: "@aztec/kv-store@npm:4.2.0-nightly.20260409" +"@aztec/kv-store@npm:4.2.0-nightly.20260410": + version: 4.2.0-nightly.20260410 + resolution: "@aztec/kv-store@npm:4.2.0-nightly.20260410" dependencies: - "@aztec/constants": "npm:4.2.0-nightly.20260409" - "@aztec/ethereum": "npm:4.2.0-nightly.20260409" - "@aztec/foundation": "npm:4.2.0-nightly.20260409" - "@aztec/native": "npm:4.2.0-nightly.20260409" - "@aztec/stdlib": "npm:4.2.0-nightly.20260409" + "@aztec/constants": "npm:4.2.0-nightly.20260410" + "@aztec/ethereum": "npm:4.2.0-nightly.20260410" + "@aztec/foundation": "npm:4.2.0-nightly.20260410" + "@aztec/native": "npm:4.2.0-nightly.20260410" + "@aztec/stdlib": "npm:4.2.0-nightly.20260410" idb: "npm:^8.0.0" lmdb: "npm:^3.2.0" msgpackr: "npm:^1.11.2" ohash: "npm:^2.0.11" ordered-binary: "npm:^1.5.3" - checksum: 10c0/f9d60645a4c686e2091a330965d7c7296fa508dde1ce649dccf5d16e02dee57ce6181a253a7596c2a7e28c9f3fc2616096032dc3cd81cbeec98cc8bf0f6e99ac + checksum: 10c0/735b7a2c02b72ef6b2dffea6c3753df0a28927ea7075da61998837315de3b1cd07cb6cd47d863fcaf05123956ec24be5d98da5137ee1068a578ef9e574452b33 languageName: node linkType: hard -"@aztec/l1-artifacts@npm:4.2.0-nightly.20260409": - version: 4.2.0-nightly.20260409 - resolution: "@aztec/l1-artifacts@npm:4.2.0-nightly.20260409" +"@aztec/l1-artifacts@npm:4.2.0-nightly.20260410": + version: 4.2.0-nightly.20260410 + resolution: "@aztec/l1-artifacts@npm:4.2.0-nightly.20260410" dependencies: tslib: "npm:^2.4.0" - checksum: 10c0/70abe9e54be2f96d782d055be99716ec51593bae5d19300531bbc61d86e8823a8cf1d1356aed6b9634a0d553646353351bf7d220bc2d962b98b3b861f5fe4e90 + checksum: 10c0/f88b5ca49cfd7d64e8863c7f33590eea75b440432dfe0017a03946efdcc4f85a8796d6d23b0f8af5f6532a5f7007646d3512e2f7de7c042722a05e11a5ac4c1b languageName: node linkType: hard -"@aztec/merkle-tree@npm:4.2.0-nightly.20260409": - version: 4.2.0-nightly.20260409 - resolution: "@aztec/merkle-tree@npm:4.2.0-nightly.20260409" +"@aztec/merkle-tree@npm:4.2.0-nightly.20260410": + version: 4.2.0-nightly.20260410 + resolution: "@aztec/merkle-tree@npm:4.2.0-nightly.20260410" dependencies: - "@aztec/foundation": "npm:4.2.0-nightly.20260409" - "@aztec/kv-store": "npm:4.2.0-nightly.20260409" - "@aztec/stdlib": "npm:4.2.0-nightly.20260409" + "@aztec/foundation": "npm:4.2.0-nightly.20260410" + "@aztec/kv-store": "npm:4.2.0-nightly.20260410" + "@aztec/stdlib": "npm:4.2.0-nightly.20260410" sha256: "npm:^0.2.0" tslib: "npm:^2.4.0" - checksum: 10c0/04a967807ef717ed7833083af4b85729635c74e88e9083b242db12617a44c4f9874cf1f15ca3b1c0a024a154ea64867aad69159565965ca5204abe9ca52b9aed + checksum: 10c0/fc7a624e9ad1fb800f8a66bd92fc43375aa6575cea01451c5739d5ae30f16d206bfdce445db0467a72f969b5066282f44b09179b6e0eacfca714e66c36fa0fd5 languageName: node linkType: hard -"@aztec/native@npm:4.2.0-nightly.20260409": - version: 4.2.0-nightly.20260409 - resolution: "@aztec/native@npm:4.2.0-nightly.20260409" +"@aztec/native@npm:4.2.0-nightly.20260410": + version: 4.2.0-nightly.20260410 + resolution: "@aztec/native@npm:4.2.0-nightly.20260410" dependencies: - "@aztec/bb.js": "npm:4.2.0-nightly.20260409" - "@aztec/foundation": "npm:4.2.0-nightly.20260409" + "@aztec/bb.js": "npm:4.2.0-nightly.20260410" + "@aztec/foundation": "npm:4.2.0-nightly.20260410" msgpackr: "npm:^1.11.2" - checksum: 10c0/6a4c456c492e5e3db1006e8c6b2afa205f5e06ec7669630293ddada692949b90ba909e6a5a6432785a26d3fc525768f8275abf9d895c4612097e19daebb136f5 + checksum: 10c0/6c3b7dcba91b4fa7a91f6886c43e7f36bdd880f6e856e52ea58220f893f9e6675cb2fb8b7e5a767ba71efa0319188c9a770774fc8e1ffb86f76bb931e3a62953 languageName: node linkType: hard -"@aztec/noir-acvm_js@npm:4.2.0-nightly.20260409": - version: 4.2.0-nightly.20260409 - resolution: "@aztec/noir-acvm_js@npm:4.2.0-nightly.20260409" - checksum: 10c0/4a37836f14f32b567a869a0544137868afd24b57dd8fda15fd69ebb66dd7d235a3389fcbf56d62d1792ff56c57bb7bbf25b8ed172cbc959f5c1322aad480b27f +"@aztec/noir-acvm_js@npm:4.2.0-nightly.20260410": + version: 4.2.0-nightly.20260410 + resolution: "@aztec/noir-acvm_js@npm:4.2.0-nightly.20260410" + checksum: 10c0/ab212f436b8413f4b1b77f60cbb3a7e22407e668143b61d436180e68f4c01659fa29c0d2533a1114abcd0b23fb833971b88826eeed013cf29f68c4a831e66e0a languageName: node linkType: hard -"@aztec/noir-contracts.js@npm:4.2.0-nightly.20260409": - version: 4.2.0-nightly.20260409 - resolution: "@aztec/noir-contracts.js@npm:4.2.0-nightly.20260409" +"@aztec/noir-contracts.js@npm:4.2.0-nightly.20260410": + version: 4.2.0-nightly.20260410 + resolution: "@aztec/noir-contracts.js@npm:4.2.0-nightly.20260410" dependencies: - "@aztec/aztec.js": "npm:4.2.0-nightly.20260409" + "@aztec/aztec.js": "npm:4.2.0-nightly.20260410" tslib: "npm:^2.4.0" - checksum: 10c0/170e7921583acdb39ef1287512bc9fa2a9f69417167b214d4b44013aa5c1cda3c3654bbd541dbc1e0c20bf3bc292d1bb9a60ed3cbdf6c4631152ecdfe34b9889 + checksum: 10c0/2caa053d9b7c2be0ba54751eb1d1cad0f3061bed9d8d1f31d89b3fbba4b7d150b586404e7caed79bd8b97af05b88767bc1877be7f90d07d1e8aa5fe8e3f3d446 languageName: node linkType: hard -"@aztec/noir-noir_codegen@npm:4.2.0-nightly.20260409": - version: 4.2.0-nightly.20260409 - resolution: "@aztec/noir-noir_codegen@npm:4.2.0-nightly.20260409" +"@aztec/noir-noir_codegen@npm:4.2.0-nightly.20260410": + version: 4.2.0-nightly.20260410 + resolution: "@aztec/noir-noir_codegen@npm:4.2.0-nightly.20260410" dependencies: - "@aztec/noir-types": "npm:4.2.0-nightly.20260409" + "@aztec/noir-types": "npm:4.2.0-nightly.20260410" glob: "npm:^13.0.0" ts-command-line-args: "npm:^2.5.1" bin: noir-codegen: lib/main.js - checksum: 10c0/907d182662dc1faa9c8df2ed72baf3f07ef73af2cc6bac3897252249fd0a47d4f22a5cfda0726253c989e394aef77de984e48247686c6d5ccadb93c348127e09 + checksum: 10c0/077b1ab8bcd6e313a38d55b3493aa61102fab6fb0c129e4fe8fc9b97e03ca74765201b379ab9d02739f66bd4ef3710385fd3c33ff9149ef1bd325810079481c5 languageName: node linkType: hard -"@aztec/noir-noirc_abi@npm:4.2.0-nightly.20260409": - version: 4.2.0-nightly.20260409 - resolution: "@aztec/noir-noirc_abi@npm:4.2.0-nightly.20260409" +"@aztec/noir-noirc_abi@npm:4.2.0-nightly.20260410": + version: 4.2.0-nightly.20260410 + resolution: "@aztec/noir-noirc_abi@npm:4.2.0-nightly.20260410" dependencies: - "@aztec/noir-types": "npm:4.2.0-nightly.20260409" - checksum: 10c0/77e27a25d500070cf230fb7aaf747eed46953eeaddbc5ef9537e4c87163f15b24b7fc8a881a84a6b90f043dcfdfa402a2f359e98bd8348fd340fff38c2a297e6 + "@aztec/noir-types": "npm:4.2.0-nightly.20260410" + checksum: 10c0/74c4a0edea3de54cbb4e770374b834eb86e91a686590bbfe8196d67b2aa249e0baf77a6a27b8e163c5b9d393fc6749fb4ac5c000fd85bcd00a527fb9c8798819 languageName: node linkType: hard -"@aztec/noir-protocol-circuits-types@npm:4.2.0-nightly.20260409": - version: 4.2.0-nightly.20260409 - resolution: "@aztec/noir-protocol-circuits-types@npm:4.2.0-nightly.20260409" +"@aztec/noir-protocol-circuits-types@npm:4.2.0-nightly.20260410": + version: 4.2.0-nightly.20260410 + resolution: "@aztec/noir-protocol-circuits-types@npm:4.2.0-nightly.20260410" dependencies: - "@aztec/blob-lib": "npm:4.2.0-nightly.20260409" - "@aztec/constants": "npm:4.2.0-nightly.20260409" - "@aztec/foundation": "npm:4.2.0-nightly.20260409" - "@aztec/noir-acvm_js": "npm:4.2.0-nightly.20260409" - "@aztec/noir-noir_codegen": "npm:4.2.0-nightly.20260409" - "@aztec/noir-noirc_abi": "npm:4.2.0-nightly.20260409" - "@aztec/noir-types": "npm:4.2.0-nightly.20260409" - "@aztec/stdlib": "npm:4.2.0-nightly.20260409" + "@aztec/blob-lib": "npm:4.2.0-nightly.20260410" + "@aztec/constants": "npm:4.2.0-nightly.20260410" + "@aztec/foundation": "npm:4.2.0-nightly.20260410" + "@aztec/noir-acvm_js": "npm:4.2.0-nightly.20260410" + "@aztec/noir-noir_codegen": "npm:4.2.0-nightly.20260410" + "@aztec/noir-noirc_abi": "npm:4.2.0-nightly.20260410" + "@aztec/noir-types": "npm:4.2.0-nightly.20260410" + "@aztec/stdlib": "npm:4.2.0-nightly.20260410" change-case: "npm:^5.4.4" tslib: "npm:^2.4.0" - checksum: 10c0/b20abeaee9760e56e4ed0061c22c0975288bde3e5873925bd23aca0aa8945190408b510405954870e7ea368f0d9d81591f92d11ad68caac65ca6b7590e562104 + checksum: 10c0/d2d032518eb549f5228abe3de95d8b0f5ff15d25352047c1b0ac7e022c1902d407dd8407c0dca54ee3cdd8f3d1bf103614ee3ad0cd68a5c9962f8b6121c761d9 languageName: node linkType: hard -"@aztec/noir-types@npm:4.2.0-nightly.20260409": - version: 4.2.0-nightly.20260409 - resolution: "@aztec/noir-types@npm:4.2.0-nightly.20260409" - checksum: 10c0/2ae30e4bb5da85d2b609c3f9c448f780a21d9140d25fcb705ce7727da5ebdeea14c4cc3751717a3a188e2f143342b13feba483b1d4b44d0b400bbcd637dca6a9 +"@aztec/noir-types@npm:4.2.0-nightly.20260410": + version: 4.2.0-nightly.20260410 + resolution: "@aztec/noir-types@npm:4.2.0-nightly.20260410" + checksum: 10c0/6e06c47e06131d859428ec5f2ffa70ecbf5d692b9081ba5e73f30aca5af55c994dfdc052d77b5a374d6d4dd593559599053e7beab974d221d0edbacf5ef4e59f languageName: node linkType: hard -"@aztec/protocol-contracts@npm:4.2.0-nightly.20260409": - version: 4.2.0-nightly.20260409 - resolution: "@aztec/protocol-contracts@npm:4.2.0-nightly.20260409" +"@aztec/protocol-contracts@npm:4.2.0-nightly.20260410": + version: 4.2.0-nightly.20260410 + resolution: "@aztec/protocol-contracts@npm:4.2.0-nightly.20260410" dependencies: - "@aztec/constants": "npm:4.2.0-nightly.20260409" - "@aztec/foundation": "npm:4.2.0-nightly.20260409" - "@aztec/stdlib": "npm:4.2.0-nightly.20260409" + "@aztec/constants": "npm:4.2.0-nightly.20260410" + "@aztec/foundation": "npm:4.2.0-nightly.20260410" + "@aztec/stdlib": "npm:4.2.0-nightly.20260410" lodash.chunk: "npm:^4.2.0" lodash.omit: "npm:^4.5.0" tslib: "npm:^2.4.0" - checksum: 10c0/e26a899cad51766f907d99fb013c80be48d61367a9d1ed7a87dca4f2725b74401c6cdbc429dcb525745799976c77cc64bf5ddb2804709309604fdc975a6cf404 - languageName: node - linkType: hard - -"@aztec/pxe@npm:4.2.0-nightly.20260409": - version: 4.2.0-nightly.20260409 - resolution: "@aztec/pxe@npm:4.2.0-nightly.20260409" - dependencies: - "@aztec/bb-prover": "npm:4.2.0-nightly.20260409" - "@aztec/bb.js": "npm:4.2.0-nightly.20260409" - "@aztec/builder": "npm:4.2.0-nightly.20260409" - "@aztec/constants": "npm:4.2.0-nightly.20260409" - "@aztec/ethereum": "npm:4.2.0-nightly.20260409" - "@aztec/foundation": "npm:4.2.0-nightly.20260409" - "@aztec/key-store": "npm:4.2.0-nightly.20260409" - "@aztec/kv-store": "npm:4.2.0-nightly.20260409" - "@aztec/noir-protocol-circuits-types": "npm:4.2.0-nightly.20260409" - "@aztec/noir-types": "npm:4.2.0-nightly.20260409" - "@aztec/protocol-contracts": "npm:4.2.0-nightly.20260409" - "@aztec/simulator": "npm:4.2.0-nightly.20260409" - "@aztec/stdlib": "npm:4.2.0-nightly.20260409" + checksum: 10c0/dde5342897ea944428bc84fa1216d9aed4066d9d7f5b50918de450d8826ab112cb1b44c18cbf7e85730357dcf94a4fbd5dc0e038764b0ff6f79479d862085766 + languageName: node + linkType: hard + +"@aztec/pxe@npm:4.2.0-nightly.20260410, @aztec/pxe@npm:v4.2.0-nightly.20260410": + version: 4.2.0-nightly.20260410 + resolution: "@aztec/pxe@npm:4.2.0-nightly.20260410" + dependencies: + "@aztec/bb-prover": "npm:4.2.0-nightly.20260410" + "@aztec/bb.js": "npm:4.2.0-nightly.20260410" + "@aztec/builder": "npm:4.2.0-nightly.20260410" + "@aztec/constants": "npm:4.2.0-nightly.20260410" + "@aztec/ethereum": "npm:4.2.0-nightly.20260410" + "@aztec/foundation": "npm:4.2.0-nightly.20260410" + "@aztec/key-store": "npm:4.2.0-nightly.20260410" + "@aztec/kv-store": "npm:4.2.0-nightly.20260410" + "@aztec/noir-protocol-circuits-types": "npm:4.2.0-nightly.20260410" + "@aztec/noir-types": "npm:4.2.0-nightly.20260410" + "@aztec/protocol-contracts": "npm:4.2.0-nightly.20260410" + "@aztec/simulator": "npm:4.2.0-nightly.20260410" + "@aztec/stdlib": "npm:4.2.0-nightly.20260410" koa: "npm:^2.16.1" koa-router: "npm:^13.1.1" lodash.omit: "npm:^4.5.0" @@ -981,45 +981,45 @@ __metadata: viem: "npm:@aztec/viem@2.38.2" bin: pxe: dest/bin/index.js - checksum: 10c0/5f882ffc82c109dd7bbf44bc71bd39706d07c5fa4ff073f6548370d9199a05737c12dd8998f0acf1026a2803dfbfa212a330f383414e5a6e8e10a5be6f72d983 + checksum: 10c0/2415bc886a469b212e9000f6b3361d5767f60ab1acdd403f027bc7c1888156337947a08e5b7c1dc106bd3913c185398325b8dc7242346781bc3695713c0574ee languageName: node linkType: hard -"@aztec/simulator@npm:4.2.0-nightly.20260409": - version: 4.2.0-nightly.20260409 - resolution: "@aztec/simulator@npm:4.2.0-nightly.20260409" +"@aztec/simulator@npm:4.2.0-nightly.20260410": + version: 4.2.0-nightly.20260410 + resolution: "@aztec/simulator@npm:4.2.0-nightly.20260410" dependencies: - "@aztec/constants": "npm:4.2.0-nightly.20260409" - "@aztec/foundation": "npm:4.2.0-nightly.20260409" - "@aztec/native": "npm:4.2.0-nightly.20260409" - "@aztec/noir-acvm_js": "npm:4.2.0-nightly.20260409" - "@aztec/noir-noirc_abi": "npm:4.2.0-nightly.20260409" - "@aztec/noir-protocol-circuits-types": "npm:4.2.0-nightly.20260409" - "@aztec/noir-types": "npm:4.2.0-nightly.20260409" - "@aztec/protocol-contracts": "npm:4.2.0-nightly.20260409" - "@aztec/stdlib": "npm:4.2.0-nightly.20260409" - "@aztec/telemetry-client": "npm:4.2.0-nightly.20260409" - "@aztec/world-state": "npm:4.2.0-nightly.20260409" + "@aztec/constants": "npm:4.2.0-nightly.20260410" + "@aztec/foundation": "npm:4.2.0-nightly.20260410" + "@aztec/native": "npm:4.2.0-nightly.20260410" + "@aztec/noir-acvm_js": "npm:4.2.0-nightly.20260410" + "@aztec/noir-noirc_abi": "npm:4.2.0-nightly.20260410" + "@aztec/noir-protocol-circuits-types": "npm:4.2.0-nightly.20260410" + "@aztec/noir-types": "npm:4.2.0-nightly.20260410" + "@aztec/protocol-contracts": "npm:4.2.0-nightly.20260410" + "@aztec/stdlib": "npm:4.2.0-nightly.20260410" + "@aztec/telemetry-client": "npm:4.2.0-nightly.20260410" + "@aztec/world-state": "npm:4.2.0-nightly.20260410" lodash.clonedeep: "npm:^4.5.0" lodash.merge: "npm:^4.6.2" tslib: "npm:^2.4.0" - checksum: 10c0/a03f137aec9370cb5a4844b43fa8d56aa6db43d5022934b6bf13d631e82d1d648389fbfbd5cc5b21784537c2d088a9be4adf02e4e18b749be9f426c8c9780fc7 + checksum: 10c0/57dcb1117b1f596bcb6edae374687f65163d265ebc23efba30e93289ad20ade025c133b2c226559f631de36ffe1ae604914f77f945ecff5388a8393147f15cde languageName: node linkType: hard -"@aztec/stdlib@npm:4.2.0-nightly.20260409": - version: 4.2.0-nightly.20260409 - resolution: "@aztec/stdlib@npm:4.2.0-nightly.20260409" +"@aztec/stdlib@npm:4.2.0-nightly.20260410, @aztec/stdlib@npm:v4.2.0-nightly.20260410": + version: 4.2.0-nightly.20260410 + resolution: "@aztec/stdlib@npm:4.2.0-nightly.20260410" dependencies: "@aws-sdk/client-s3": "npm:^3.892.0" - "@aztec/bb.js": "npm:4.2.0-nightly.20260409" - "@aztec/blob-lib": "npm:4.2.0-nightly.20260409" - "@aztec/constants": "npm:4.2.0-nightly.20260409" - "@aztec/ethereum": "npm:4.2.0-nightly.20260409" - "@aztec/foundation": "npm:4.2.0-nightly.20260409" - "@aztec/l1-artifacts": "npm:4.2.0-nightly.20260409" - "@aztec/noir-noirc_abi": "npm:4.2.0-nightly.20260409" - "@aztec/validator-ha-signer": "npm:4.2.0-nightly.20260409" + "@aztec/bb.js": "npm:4.2.0-nightly.20260410" + "@aztec/blob-lib": "npm:4.2.0-nightly.20260410" + "@aztec/constants": "npm:4.2.0-nightly.20260410" + "@aztec/ethereum": "npm:4.2.0-nightly.20260410" + "@aztec/foundation": "npm:4.2.0-nightly.20260410" + "@aztec/l1-artifacts": "npm:4.2.0-nightly.20260410" + "@aztec/noir-noirc_abi": "npm:4.2.0-nightly.20260410" + "@aztec/validator-ha-signer": "npm:4.2.0-nightly.20260410" "@google-cloud/storage": "npm:^7.15.0" axios: "npm:^1.13.5" json-stringify-deterministic: "npm:1.0.12" @@ -1032,16 +1032,16 @@ __metadata: tslib: "npm:^2.4.0" viem: "npm:@aztec/viem@2.38.2" zod: "npm:^3.23.8" - checksum: 10c0/8465234450908c6ad8c402d0095ecc6e3e6b83dec8c4f635d485e04a3d28ba9d3ecad61e6f497e97ea7e77c9808ddab11e4ef4c51b76398496db30d2ef5c2525 + checksum: 10c0/c94829149f6497cbe8900c1ef0367f324a5c09fa003eadfedcca08b7c1e0e0c292db2f1eeab53e100548338d98102a568741bb33cd38da26d33333b00293926b languageName: node linkType: hard -"@aztec/telemetry-client@npm:4.2.0-nightly.20260409": - version: 4.2.0-nightly.20260409 - resolution: "@aztec/telemetry-client@npm:4.2.0-nightly.20260409" +"@aztec/telemetry-client@npm:4.2.0-nightly.20260410": + version: 4.2.0-nightly.20260410 + resolution: "@aztec/telemetry-client@npm:4.2.0-nightly.20260410" dependencies: - "@aztec/foundation": "npm:4.2.0-nightly.20260409" - "@aztec/stdlib": "npm:4.2.0-nightly.20260409" + "@aztec/foundation": "npm:4.2.0-nightly.20260410" + "@aztec/stdlib": "npm:4.2.0-nightly.20260410" "@opentelemetry/api": "npm:^1.9.0" "@opentelemetry/api-logs": "npm:^0.55.0" "@opentelemetry/core": "npm:^1.28.0" @@ -1058,70 +1058,70 @@ __metadata: "@opentelemetry/semantic-conventions": "npm:^1.28.0" prom-client: "npm:^15.1.3" viem: "npm:@aztec/viem@2.38.2" - checksum: 10c0/370129219ea5083ca4a65048b2ead3d128b1621b399ee5d429ff18858fd32d4644f7cc6a95e8b2cdf5e1d227b9dc030375af72ec9b2bbc843cc10ac2991557df + checksum: 10c0/2907a51f6e276791a7624ae5e4ea195b3098c037792e34f14dd442e0cc805cd7e83626ccbd057c79b0d157ba19a3d46c0f0ab033c52c80548f4faee2c819c7e7 languageName: node linkType: hard -"@aztec/validator-ha-signer@npm:4.2.0-nightly.20260409": - version: 4.2.0-nightly.20260409 - resolution: "@aztec/validator-ha-signer@npm:4.2.0-nightly.20260409" +"@aztec/validator-ha-signer@npm:4.2.0-nightly.20260410": + version: 4.2.0-nightly.20260410 + resolution: "@aztec/validator-ha-signer@npm:4.2.0-nightly.20260410" dependencies: - "@aztec/ethereum": "npm:4.2.0-nightly.20260409" - "@aztec/foundation": "npm:4.2.0-nightly.20260409" + "@aztec/ethereum": "npm:4.2.0-nightly.20260410" + "@aztec/foundation": "npm:4.2.0-nightly.20260410" node-pg-migrate: "npm:^8.0.4" pg: "npm:^8.11.3" tslib: "npm:^2.4.0" zod: "npm:^3.23.8" - checksum: 10c0/1b3a7b366d47bd1af6b6f0aa1f015e86a7b63b080c5378ef0dbd0cde1875af0ba48f21cae6797f373f40167dc51178ee1d6be5e0f94bf614a14fa5a7013b5e11 + checksum: 10c0/bc875b107f7af49828c734412c62ec5751163effc342ac521544c556d5f7081a1f213eea1378a20edf94d2bf5a73a01c22dafd6ac76bc7c543000d37abc6d3ad languageName: node linkType: hard -"@aztec/wallet-sdk@npm:4.2.0-nightly.20260409": - version: 4.2.0-nightly.20260409 - resolution: "@aztec/wallet-sdk@npm:4.2.0-nightly.20260409" +"@aztec/wallet-sdk@npm:4.2.0-nightly.20260410, @aztec/wallet-sdk@npm:v4.2.0-nightly.20260410": + version: 4.2.0-nightly.20260410 + resolution: "@aztec/wallet-sdk@npm:4.2.0-nightly.20260410" dependencies: - "@aztec/aztec.js": "npm:4.2.0-nightly.20260409" - "@aztec/constants": "npm:4.2.0-nightly.20260409" - "@aztec/entrypoints": "npm:4.2.0-nightly.20260409" - "@aztec/foundation": "npm:4.2.0-nightly.20260409" - "@aztec/pxe": "npm:4.2.0-nightly.20260409" - "@aztec/stdlib": "npm:4.2.0-nightly.20260409" - checksum: 10c0/620fe6900382480b53369a70ba07de714a9db4d40f9bdb6723cea560a3887e95ca50d9e7b2d9966ae799cef4b6321da7b20f038b5bc745553f1e4ce39e3ca2fd + "@aztec/aztec.js": "npm:4.2.0-nightly.20260410" + "@aztec/constants": "npm:4.2.0-nightly.20260410" + "@aztec/entrypoints": "npm:4.2.0-nightly.20260410" + "@aztec/foundation": "npm:4.2.0-nightly.20260410" + "@aztec/pxe": "npm:4.2.0-nightly.20260410" + "@aztec/stdlib": "npm:4.2.0-nightly.20260410" + checksum: 10c0/50bb319790c181c9b8fb9bdcb7c4f310b2e0cd5972b62841244dcd932c35dc10f28ce2a9787cab63d6c3abee17455261048aadbdeaf1ca367513976a05a4b279 languageName: node linkType: hard -"@aztec/wallets@npm:4.2.0-nightly.20260409": - version: 4.2.0-nightly.20260409 - resolution: "@aztec/wallets@npm:4.2.0-nightly.20260409" +"@aztec/wallets@npm:4.2.0-nightly.20260410, @aztec/wallets@npm:v4.2.0-nightly.20260410": + version: 4.2.0-nightly.20260410 + resolution: "@aztec/wallets@npm:4.2.0-nightly.20260410" dependencies: - "@aztec/accounts": "npm:4.2.0-nightly.20260409" - "@aztec/aztec.js": "npm:4.2.0-nightly.20260409" - "@aztec/entrypoints": "npm:4.2.0-nightly.20260409" - "@aztec/foundation": "npm:4.2.0-nightly.20260409" - "@aztec/kv-store": "npm:4.2.0-nightly.20260409" - "@aztec/protocol-contracts": "npm:4.2.0-nightly.20260409" - "@aztec/pxe": "npm:4.2.0-nightly.20260409" - "@aztec/stdlib": "npm:4.2.0-nightly.20260409" - "@aztec/wallet-sdk": "npm:4.2.0-nightly.20260409" - checksum: 10c0/c9b9c7069335c0f622b8e08a48195ccfab657afe140ed5c2ec1c544282e9d07a1414d1c0160a98a658147f6452493706b4baa8d8f9da9ab4c9116b67d570cb70 + "@aztec/accounts": "npm:4.2.0-nightly.20260410" + "@aztec/aztec.js": "npm:4.2.0-nightly.20260410" + "@aztec/entrypoints": "npm:4.2.0-nightly.20260410" + "@aztec/foundation": "npm:4.2.0-nightly.20260410" + "@aztec/kv-store": "npm:4.2.0-nightly.20260410" + "@aztec/protocol-contracts": "npm:4.2.0-nightly.20260410" + "@aztec/pxe": "npm:4.2.0-nightly.20260410" + "@aztec/stdlib": "npm:4.2.0-nightly.20260410" + "@aztec/wallet-sdk": "npm:4.2.0-nightly.20260410" + checksum: 10c0/475f4dd52be9008bc60aad509e26c27bf133f2ac859072548c4516aa5cf86fc0aa16d0a9298667566766088d71a95b26d51e441ae8f8da7db85d1f39a54d9b88 languageName: node linkType: hard -"@aztec/world-state@npm:4.2.0-nightly.20260409": - version: 4.2.0-nightly.20260409 - resolution: "@aztec/world-state@npm:4.2.0-nightly.20260409" +"@aztec/world-state@npm:4.2.0-nightly.20260410": + version: 4.2.0-nightly.20260410 + resolution: "@aztec/world-state@npm:4.2.0-nightly.20260410" dependencies: - "@aztec/constants": "npm:4.2.0-nightly.20260409" - "@aztec/foundation": "npm:4.2.0-nightly.20260409" - "@aztec/kv-store": "npm:4.2.0-nightly.20260409" - "@aztec/merkle-tree": "npm:4.2.0-nightly.20260409" - "@aztec/native": "npm:4.2.0-nightly.20260409" - "@aztec/protocol-contracts": "npm:4.2.0-nightly.20260409" - "@aztec/stdlib": "npm:4.2.0-nightly.20260409" - "@aztec/telemetry-client": "npm:4.2.0-nightly.20260409" + "@aztec/constants": "npm:4.2.0-nightly.20260410" + "@aztec/foundation": "npm:4.2.0-nightly.20260410" + "@aztec/kv-store": "npm:4.2.0-nightly.20260410" + "@aztec/merkle-tree": "npm:4.2.0-nightly.20260410" + "@aztec/native": "npm:4.2.0-nightly.20260410" + "@aztec/protocol-contracts": "npm:4.2.0-nightly.20260410" + "@aztec/stdlib": "npm:4.2.0-nightly.20260410" + "@aztec/telemetry-client": "npm:4.2.0-nightly.20260410" tslib: "npm:^2.4.0" zod: "npm:^3.23.8" - checksum: 10c0/fc16d2974ea9ce502060ae1ce82010084ad30e2e307d4c115566eea2807cc76b0db8c89f972a7fb35fc72b40ae54593635bb2ca0c461e68fd8494877b6e84531 + checksum: 10c0/3b2f73e19729730d1c1599a8dd442a34eae0da8c14801c9ff9b63baa8d3c01dd4fc69edde33dd86ad8ac198c35a4f6d90cbc2c321650a7ac1fbe41a72cefec2b languageName: node linkType: hard @@ -1773,30 +1773,33 @@ __metadata: languageName: node linkType: hard -"@gregojuice/contracts@portal:/mnt/user-data/martin/gregojuice/packages/contracts::locator=gregoswap%40workspace%3A.": - version: 0.0.0-use.local - resolution: "@gregojuice/contracts@portal:/mnt/user-data/martin/gregojuice/packages/contracts::locator=gregoswap%40workspace%3A." +"@gregojuice/contracts@npm:0.0.12": + version: 0.0.12 + resolution: "@gregojuice/contracts@npm:0.0.12" dependencies: - "@aztec/aztec.js": "npm:4.2.0-nightly.20260409" - "@aztec/foundation": "npm:4.2.0-nightly.20260409" - "@aztec/stdlib": "npm:4.2.0-nightly.20260409" - "@aztec/wallets": "npm:4.2.0-nightly.20260409" + "@aztec/aztec.js": "npm:v4.2.0-nightly.20260410" + "@aztec/foundation": "npm:v4.2.0-nightly.20260410" + "@aztec/stdlib": "npm:v4.2.0-nightly.20260410" + "@aztec/wallets": "npm:v4.2.0-nightly.20260410" + checksum: 10c0/686fe025335c6f82d320e3642c456408d8837c58a4d26d73d551bffbcd7faf57f1dc3cee5097fb4f010fb3efda41571273f0f57a5ceed9f8c7b8e743b8d9fd27 languageName: node - linkType: soft + linkType: hard -"@gregojuice/embedded-wallet@portal:/mnt/user-data/martin/gregojuice/packages/embedded-wallet::locator=gregoswap%40workspace%3A.": - version: 0.0.0-use.local - resolution: "@gregojuice/embedded-wallet@portal:/mnt/user-data/martin/gregojuice/packages/embedded-wallet::locator=gregoswap%40workspace%3A." +"@gregojuice/embedded-wallet@npm:0.0.12": + version: 0.0.12 + resolution: "@gregojuice/embedded-wallet@npm:0.0.12" dependencies: - "@aztec/aztec.js": "npm:4.2.0-nightly.20260409" - "@aztec/entrypoints": "npm:4.2.0-nightly.20260409" - "@aztec/foundation": "npm:4.2.0-nightly.20260409" - "@aztec/pxe": "npm:4.2.0-nightly.20260409" - "@aztec/stdlib": "npm:4.2.0-nightly.20260409" - "@aztec/wallet-sdk": "npm:4.2.0-nightly.20260409" - "@aztec/wallets": "npm:4.2.0-nightly.20260409" + "@aztec/aztec.js": "npm:v4.2.0-nightly.20260410" + "@aztec/entrypoints": "npm:v4.2.0-nightly.20260410" + "@aztec/foundation": "npm:v4.2.0-nightly.20260410" + "@aztec/pxe": "npm:v4.2.0-nightly.20260410" + "@aztec/stdlib": "npm:v4.2.0-nightly.20260410" + "@aztec/wallet-sdk": "npm:v4.2.0-nightly.20260410" + "@aztec/wallets": "npm:v4.2.0-nightly.20260410" + "@gregojuice/contracts": "npm:0.0.12" + checksum: 10c0/7cd6023622e2dd94d88415da85ebd8b3f9cb4f4a7af2e4922b8508164cf3cf966940a7cb398ee4faa67c758553f35b8108417441f3dfc50e0df15b4746621b3b languageName: node - linkType: soft + linkType: hard "@hapi/bourne@npm:^3.0.0": version: 3.0.0 @@ -6069,22 +6072,23 @@ __metadata: version: 0.0.0-use.local resolution: "gregoswap@workspace:." dependencies: - "@aztec/accounts": "npm:4.2.0-nightly.20260409" - "@aztec/aztec.js": "npm:4.2.0-nightly.20260409" - "@aztec/constants": "npm:4.2.0-nightly.20260409" - "@aztec/entrypoints": "npm:4.2.0-nightly.20260409" - "@aztec/foundation": "npm:4.2.0-nightly.20260409" - "@aztec/noir-contracts.js": "npm:4.2.0-nightly.20260409" - "@aztec/protocol-contracts": "npm:4.2.0-nightly.20260409" - "@aztec/pxe": "npm:4.2.0-nightly.20260409" - "@aztec/stdlib": "npm:4.2.0-nightly.20260409" - "@aztec/wallet-sdk": "npm:4.2.0-nightly.20260409" - "@aztec/wallets": "npm:4.2.0-nightly.20260409" + "@aztec/accounts": "npm:4.2.0-nightly.20260410" + "@aztec/aztec.js": "npm:4.2.0-nightly.20260410" + "@aztec/constants": "npm:4.2.0-nightly.20260410" + "@aztec/entrypoints": "npm:4.2.0-nightly.20260410" + "@aztec/ethereum": "npm:4.2.0-nightly.20260410" + "@aztec/foundation": "npm:4.2.0-nightly.20260410" + "@aztec/noir-contracts.js": "npm:4.2.0-nightly.20260410" + "@aztec/protocol-contracts": "npm:4.2.0-nightly.20260410" + "@aztec/pxe": "npm:4.2.0-nightly.20260410" + "@aztec/stdlib": "npm:4.2.0-nightly.20260410" + "@aztec/wallet-sdk": "npm:4.2.0-nightly.20260410" + "@aztec/wallets": "npm:4.2.0-nightly.20260410" "@emotion/react": "npm:^11.14.0" "@emotion/styled": "npm:^11.14.0" "@eslint/js": "npm:^9.18.0" - "@gregojuice/contracts": "portal:/mnt/user-data/martin/gregojuice/packages/contracts" - "@gregojuice/embedded-wallet": "portal:/mnt/user-data/martin/gregojuice/packages/embedded-wallet" + "@gregojuice/contracts": "npm:0.0.12" + "@gregojuice/embedded-wallet": "npm:0.0.12" "@mui/icons-material": "npm:^6.3.1" "@mui/material": "npm:^6.3.1" "@mui/styles": "npm:^6.3.1" From aa74e8d1452eb394b2926b1ae6bf87ed83f9b57b Mon Sep 17 00:00:00 2001 From: mverzilli Date: Mon, 13 Apr 2026 10:46:15 +0000 Subject: [PATCH 15/24] update deps --- yarn.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/yarn.lock b/yarn.lock index a1d958f..f1f5139 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6090,6 +6090,7 @@ __metadata: "@aztec/aztec.js": "npm:v4.2.0-nightly.20260412" "@aztec/constants": "npm:v4.2.0-nightly.20260412" "@aztec/entrypoints": "npm:v4.2.0-nightly.20260412" + "@aztec/ethereum": "npm:4.2.0-nightly.20260412" "@aztec/foundation": "npm:v4.2.0-nightly.20260412" "@aztec/noir-contracts.js": "npm:v4.2.0-nightly.20260412" "@aztec/protocol-contracts": "npm:v4.2.0-nightly.20260412" From 1a953d632073ec143d13e080bb5e4ec4286624e0 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Mon, 13 Apr 2026 11:57:57 +0000 Subject: [PATCH 16/24] simplify token --- contracts/token/Nargo.toml | 8 ++++---- contracts/token/src/main.nr | 22 ++-------------------- scripts/deploy-subscription-fpc.ts | 6 +++--- src/services/contractService.ts | 8 +++----- 4 files changed, 12 insertions(+), 32 deletions(-) diff --git a/contracts/token/Nargo.toml b/contracts/token/Nargo.toml index 3ba9031..e12a9bf 100644 --- a/contracts/token/Nargo.toml +++ b/contracts/token/Nargo.toml @@ -4,7 +4,7 @@ authors = [""] type = "contract" [dependencies] -aztec = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-nightly.20260410", directory = "noir-projects/aztec-nr/aztec" } -uint_note = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-nightly.20260410", directory = "noir-projects/aztec-nr/uint-note" } -compressed_string = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-nightly.20260410", directory = "noir-projects/aztec-nr/compressed-string" } -balance_set = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-nightly.20260410", directory = "noir-projects/aztec-nr/balance-set" } +aztec = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-nightly.20260412", directory = "noir-projects/aztec-nr/aztec" } +uint_note = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-nightly.20260412", directory = "noir-projects/aztec-nr/uint-note" } +compressed_string = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-nightly.20260412", directory = "noir-projects/aztec-nr/compressed-string" } +balance_set = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-nightly.20260412", directory = "noir-projects/aztec-nr/balance-set" } diff --git a/contracts/token/src/main.nr b/contracts/token/src/main.nr index f4dcadb..c3760f6 100644 --- a/contracts/token/src/main.nr +++ b/contracts/token/src/main.nr @@ -251,33 +251,15 @@ pub contract Token { ); } - #[external("private")] - fn transfer_offchain(to: AztecAddress, amount: u128) { - let from = self.msg_sender(); - - let change = self.internal.subtract_balance(from, amount, INITIAL_TRANSFER_CALL_MAX_NOTES); - self.storage.balances.at(from).add(change).deliver(MessageDelivery.OFFCHAIN); - self.storage.balances.at(to).add(amount).deliver(MessageDelivery.OFFCHAIN); - - self.emit(Transfer { from, to, amount }).deliver_to( - to, - MessageDelivery.OFFCHAIN, - ); - } - - /// FPC-friendly variant of `transfer_offchain` that takes the sender explicitly, - /// allowing the call to be sponsored (msg_sender will be the FPC, not the user). - /// Authwitness ensures the user authorized this exact transfer. #[authorize_once("from", "authwit_nonce")] #[external("private")] - fn transfer_offchain_from( + fn transfer_in_private_deliver_offchain( from: AztecAddress, to: AztecAddress, amount: u128, authwit_nonce: Field, ) { - let change = self.internal.subtract_balance(from, amount, INITIAL_TRANSFER_CALL_MAX_NOTES); - self.storage.balances.at(from).add(change).deliver(MessageDelivery.OFFCHAIN); + self.storage.balances.at(from).sub(amount).deliver(MessageDelivery.OFFCHAIN); self.storage.balances.at(to).add(amount).deliver(MessageDelivery.OFFCHAIN); self.emit(Transfer { from, to, amount }).deliver_to( diff --git a/scripts/deploy-subscription-fpc.ts b/scripts/deploy-subscription-fpc.ts index dec2ab9..94b04f4 100644 --- a/scripts/deploy-subscription-fpc.ts +++ b/scripts/deploy-subscription-fpc.ts @@ -40,7 +40,7 @@ async function main() { const ammFn = AMMContractArtifact.functions.find(f => f.name === 'swap_tokens_for_exact_tokens_from'); const ammSelector = await FunctionSelector.fromNameAndParameters(ammFn!.name, ammFn!.parameters); - const transferOffchainFn = TokenContractArtifact.functions.find(f => f.name === 'transfer_offchain_from'); + const transferOffchainFn = TokenContractArtifact.functions.find(f => f.name === 'transfer_in_private_deliver_offchain'); const transferOffchainSelector = await FunctionSelector.fromNameAndParameters( transferOffchainFn!.name, transferOffchainFn!.parameters, @@ -112,10 +112,10 @@ async function main() { .send({ from: deployer, fee: { paymentMethod } }); console.log('AMM sign_up done!'); - // Sign up transfer_offchain_from on both token contracts + // Sign up transfer_in_private_deliver_offchain on both token contracts for (const tokenKey of ['gregoCoin', 'gregoCoinPremium'] as const) { const tokenAddress = config.contracts[tokenKey]; - console.log(`Signing up ${tokenKey}.transfer_offchain_from at index 0...`); + console.log(`Signing up ${tokenKey}.transfer_in_private_deliver_offchain at index 0...`); await fpc.methods .sign_up(tokenAddress, transferOffchainSelector, 0, maxUses, maxFee, maxUsers) .send({ from: deployer, fee: { paymentMethod } }); diff --git a/src/services/contractService.ts b/src/services/contractService.ts index 593c2ef..428326a 100644 --- a/src/services/contractService.ts +++ b/src/services/contractService.ts @@ -542,7 +542,7 @@ export async function executeDrip( } /** - * Offchain message returned by transfer_offchain + * Offchain message returned by transfer_in_private_deliver_offchain */ export interface OffchainMessage { recipient: AztecAddress; @@ -572,11 +572,9 @@ export async function executeTransferOffchain( const token = contracts[tokenKey]; - // Build the FPC-friendly call (transfer_offchain_from takes the sender explicitly + - // an authwit_nonce so the wallet can authorize the FPC to dispatch on the user's behalf) const authwitNonce = Fr.random(); const call = await token.methods - .transfer_offchain_from(fromAddress, recipient, amount, authwitNonce) + .transfer_in_private_deliver_offchain(fromAddress, recipient, amount, authwitNonce) .getFunctionCall(); const configIndex = subFPC.functions[token.address.toString()]?.[call.selector.toString()]; @@ -592,7 +590,7 @@ export async function executeTransferOffchain( const rawFPC = SubscriptionFPCContract.at(fpcAddress, wallet); const fpc = new SubscriptionFPC(rawFPC); - // Create an authwitness so the FPC can call transfer_offchain_from on the user's behalf. + // Create an authwitness so the FPC can call transfer_in_private_deliver_offchain on the user's behalf. // The caller from the token contract's perspective is the FPC. const authWitness = await wallet.createAuthWit(fromAddress, { caller: fpcAddress, call }); From e2436dbc87ccc277ed4d3fc0fe33afc6d510f271 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Mon, 13 Apr 2026 12:08:49 +0000 Subject: [PATCH 17/24] remove plans --- .../2026-04-09-p2p-offchain-transfers.md | 1793 ----------------- ...026-04-09-p2p-offchain-transfers-design.md | 373 ---- 2 files changed, 2166 deletions(-) delete mode 100644 docs/superpowers/plans/2026-04-09-p2p-offchain-transfers.md delete mode 100644 docs/superpowers/specs/2026-04-09-p2p-offchain-transfers-design.md diff --git a/docs/superpowers/plans/2026-04-09-p2p-offchain-transfers.md b/docs/superpowers/plans/2026-04-09-p2p-offchain-transfers.md deleted file mode 100644 index 0dd3d86..0000000 --- a/docs/superpowers/plans/2026-04-09-p2p-offchain-transfers.md +++ /dev/null @@ -1,1793 +0,0 @@ -# P2P Offchain Transfers Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add P2P private token transfers to GregoSwap using Aztec's offchain delivery, with shareable claim links and QR codes. - -**Architecture:** Fork the standard Token contract to add a `transfer_offchain` method that delivers notes via `MessageDelivery.OFFCHAIN`. The frontend extracts offchain messages from the transaction, encodes them into URLs, and provides a claim page where recipients open the link and call `offchain_receive()` to claim tokens. - -**Tech Stack:** Noir (Aztec contracts), React 18, TypeScript, MUI, Vite, qrcode.react - -**Spec:** `docs/superpowers/specs/2026-04-09-p2p-offchain-transfers-design.md` - ---- - -## File Structure - -``` -contracts/ - token/ # NEW — fork of standard Token contract - src/main.nr # standard Token + transfer_offchain - Nargo.toml # local deps pointing to aztec-packages - amm/Nargo.toml # MOD — token dep → local fork - proof_of_password/Nargo.toml # MOD — token dep → local fork - -src/ - services/ - offchainLinkService.ts # NEW — encode/decode transfer links - sentHistoryService.ts # NEW — localStorage CRUD for sent transfers - contractService.ts # MOD — add executeTransferOffchain + parseSendError - contexts/ - send/ - reducer.ts # NEW — send state machine - SendContext.tsx # NEW — send flow orchestration - index.ts # NEW — exports - components/ - App.tsx # MOD — hash route detection, tab bar - send/ - SendContainer.tsx # NEW — send flow orchestrator - SendForm.tsx # NEW — token selector, address, amount - SendProgress.tsx # NEW — sending state indicator - LinkDisplay.tsx # NEW — copyable link + QR code - SentHistory.tsx # NEW — list of past transfers - claim/ - ClaimPage.tsx # NEW — claim flow orchestrator - ClaimProgress.tsx # NEW — claiming state indicator - ClaimSuccess.tsx # NEW — success state + CTA - main.tsx # MOD — add SendProvider -``` - ---- - -### Task 1: Fork Token Contract - -**Files:** -- Create: `contracts/token/Nargo.toml` -- Create: `contracts/token/src/main.nr` -- Modify: `contracts/amm/Nargo.toml` -- Modify: `contracts/proof_of_password/Nargo.toml` - -- [ ] **Step 1: Create Nargo.toml for the forked token** - -Create `contracts/token/Nargo.toml`: - -```toml -[package] -name = "token_contract" -authors = [""] -type = "contract" - -[dependencies] -aztec = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-aztecnr-rc.2", directory = "noir-projects/aztec-nr/aztec" } -uint_note = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-aztecnr-rc.2", directory = "noir-projects/aztec-nr/uint-note" } -compressed_string = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-aztecnr-rc.2", directory = "noir-projects/aztec-nr/compressed-string" } -balance_set = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-aztecnr-rc.2", directory = "noir-projects/aztec-nr/balance-set" } -``` - -- [ ] **Step 2: Copy the standard Token contract source** - -Copy the Token contract main.nr from the Aztec monorepo: - -```bash -cp /mnt/user-data/martin/aztec-packages/noir-projects/noir-contracts/contracts/app/token_contract/src/main.nr contracts/token/src/main.nr -``` - -Do NOT copy the test files — we don't need them for the fork. - -- [ ] **Step 3: Add the `transfer_offchain` method** - -In `contracts/token/src/main.nr`, add the following method directly after the existing `transfer` function (after line ~254 in the original). The method is identical to `transfer` but uses `MessageDelivery.OFFCHAIN` for all three deliveries: - -```noir - #[external("private")] - fn transfer_offchain(to: AztecAddress, amount: u128) { - let from = self.msg_sender(); - - let change = self.internal.subtract_balance(from, amount, INITIAL_TRANSFER_CALL_MAX_NOTES); - self.storage.balances.at(from).add(change).deliver(MessageDelivery.OFFCHAIN); - self.storage.balances.at(to).add(amount).deliver(MessageDelivery.OFFCHAIN); - - self.emit(Transfer { from, to, amount }).deliver_to( - to, - MessageDelivery.OFFCHAIN, - ); - } -``` - -Note: `MessageDelivery` is already imported at line 27 of the standard Token contract (`messages::message_delivery::MessageDelivery`). No new imports needed. - -- [ ] **Step 4: Remove the test module reference** - -The copied `main.nr` starts with `mod test;` on line 3. Remove this line since we didn't copy the test files. Alternatively, create an empty `contracts/token/src/test.nr` with just `// Tests omitted in fork`. - -- [ ] **Step 5: Update AMM Nargo.toml to use local fork** - -In `contracts/amm/Nargo.toml`, change the token dependency from: - -```toml -token = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-aztecnr-rc.2", directory = "noir-projects/noir-contracts/contracts/app/token_contract" } -``` - -to: - -```toml -token = { path = "../token" } -``` - -- [ ] **Step 6: Update PoP Nargo.toml to use local fork** - -In `contracts/proof_of_password/Nargo.toml`, change the token dependency from: - -```toml -token = { git = "https://github.com/AztecProtocol/aztec-packages/", tag = "v4.2.0-aztecnr-rc.2", directory = "noir-projects/noir-contracts/contracts/app/token_contract" } -``` - -to: - -```toml -token = { path = "../token" } -``` - -- [ ] **Step 7: Compile contracts** - -```bash -yarn compile:contracts -``` - -Expected: All three contracts compile successfully. The AMM and PoP contracts should work identically since the fork is additive-only. - -- [ ] **Step 8: Commit** - -```bash -git add contracts/token/ contracts/amm/Nargo.toml contracts/proof_of_password/Nargo.toml -git commit -m "feat: fork Token contract with transfer_offchain method - -Add local fork of standard Token contract that adds a transfer_offchain -method using MessageDelivery.OFFCHAIN for all note and event deliveries. -Update AMM and PoP contracts to use local fork." -``` - ---- - -### Task 2: Offchain Link Service - -**Files:** -- Create: `src/services/offchainLinkService.ts` - -- [ ] **Step 1: Create the link service** - -Create `src/services/offchainLinkService.ts`: - -```typescript -/** - * Offchain Link Service - * Encodes/decodes offchain transfer messages into shareable URLs - */ - -export interface TransferLink { - token: 'gc' | 'gcp'; - amount: string; - recipient: string; - contractAddress: string; - txHash: string; - anchorBlockTimestamp: string; - payload: string[]; -} - -export function encodeTransferLink(data: TransferLink): string { - const json = JSON.stringify(data); - const encoded = btoa(json) - .replace(/\+/g, '-') - .replace(/\//g, '_') - .replace(/=/g, ''); - return `${window.location.origin}/#/claim/${encoded}`; -} - -export function decodeTransferLink(encoded: string): TransferLink { - const base64 = encoded.replace(/-/g, '+').replace(/_/g, '/'); - const json = atob(base64); - return JSON.parse(json) as TransferLink; -} - -export function extractClaimPayload(): TransferLink | null { - const hash = window.location.hash; - const prefix = '#/claim/'; - if (!hash.startsWith(prefix)) { - return null; - } - try { - return decodeTransferLink(hash.slice(prefix.length)); - } catch { - return null; - } -} - -export function isClaimRoute(): boolean { - return window.location.hash.startsWith('#/claim/'); -} -``` - -- [ ] **Step 2: Commit** - -```bash -git add src/services/offchainLinkService.ts -git commit -m "feat: add offchain link service for encoding/decoding transfer URLs" -``` - ---- - -### Task 3: Sent History Service - -**Files:** -- Create: `src/services/sentHistoryService.ts` - -- [ ] **Step 1: Create the sent history service** - -Create `src/services/sentHistoryService.ts`: - -```typescript -/** - * Sent History Service - * localStorage CRUD for tracking sent offchain transfers - */ - -export type SentTransferStatus = 'pending' | 'confirmed' | 'expired'; - -export interface SentTransfer { - id: string; - token: 'gc' | 'gcp'; - amount: string; - recipient: string; - link: string; - createdAt: number; - status: SentTransferStatus; -} - -function storageKey(senderAddress: string): string { - return `gregoswap_sent_transfers_${senderAddress}`; -} - -export function getSentTransfers(senderAddress: string): SentTransfer[] { - try { - const raw = localStorage.getItem(storageKey(senderAddress)); - if (!raw) return []; - return JSON.parse(raw) as SentTransfer[]; - } catch { - return []; - } -} - -export function addSentTransfer(senderAddress: string, transfer: SentTransfer): void { - const existing = getSentTransfers(senderAddress); - existing.unshift(transfer); - localStorage.setItem(storageKey(senderAddress), JSON.stringify(existing)); -} - -export function updateSentTransferStatus( - senderAddress: string, - transferId: string, - status: SentTransferStatus, -): void { - const transfers = getSentTransfers(senderAddress); - const index = transfers.findIndex(t => t.id === transferId); - if (index !== -1) { - transfers[index].status = status; - localStorage.setItem(storageKey(senderAddress), JSON.stringify(transfers)); - } -} -``` - -- [ ] **Step 2: Commit** - -```bash -git add src/services/sentHistoryService.ts -git commit -m "feat: add sent history service for tracking offchain transfers" -``` - ---- - -### Task 4: Send Reducer and Context - -**Files:** -- Create: `src/contexts/send/reducer.ts` -- Create: `src/contexts/send/SendContext.tsx` -- Create: `src/contexts/send/index.ts` -- Modify: `src/main.tsx` - -- [ ] **Step 1: Create the send reducer** - -Create `src/contexts/send/reducer.ts` following the existing pattern from `src/contexts/swap/reducer.ts`: - -```typescript -/** - * Send Reducer - * Manages send flow state and transaction phases - */ - -import { createReducerHook, type ActionsFrom } from '../utils'; - -// ============================================================================= -// State -// ============================================================================= - -export type SendPhase = 'idle' | 'sending' | 'generating_link' | 'link_ready' | 'error'; - -export interface SendState { - token: 'gc' | 'gcp'; - recipientAddress: string; - amount: string; - phase: SendPhase; - error: string | null; - generatedLink: string | null; -} - -export const initialSendState: SendState = { - token: 'gc', - recipientAddress: '', - amount: '', - phase: 'idle', - error: null, - generatedLink: null, -}; - -// ============================================================================= -// Actions -// ============================================================================= - -export const sendActions = { - setToken: (token: 'gc' | 'gcp') => ({ type: 'send/SET_TOKEN' as const, token }), - setRecipientAddress: (address: string) => ({ type: 'send/SET_RECIPIENT' as const, address }), - setAmount: (amount: string) => ({ type: 'send/SET_AMOUNT' as const, amount }), - startSend: () => ({ type: 'send/START_SEND' as const }), - generatingLink: () => ({ type: 'send/GENERATING_LINK' as const }), - linkReady: (link: string) => ({ type: 'send/LINK_READY' as const, link }), - sendError: (error: string) => ({ type: 'send/SEND_ERROR' as const, error }), - dismissError: () => ({ type: 'send/DISMISS_ERROR' as const }), - reset: () => ({ type: 'send/RESET' as const }), -}; - -export type SendAction = ActionsFrom; - -// ============================================================================= -// Reducer -// ============================================================================= - -export function sendReducer(state: SendState, action: SendAction): SendState { - switch (action.type) { - case 'send/SET_TOKEN': - return { ...state, token: action.token }; - - case 'send/SET_RECIPIENT': - return { ...state, recipientAddress: action.address }; - - case 'send/SET_AMOUNT': - return { ...state, amount: action.amount }; - - case 'send/START_SEND': - return { ...state, phase: 'sending', error: null, generatedLink: null }; - - case 'send/GENERATING_LINK': - return { ...state, phase: 'generating_link' }; - - case 'send/LINK_READY': - return { ...state, phase: 'link_ready', generatedLink: action.link }; - - case 'send/SEND_ERROR': - return { ...state, phase: 'error', error: action.error }; - - case 'send/DISMISS_ERROR': - return { ...state, phase: 'idle', error: null }; - - case 'send/RESET': - return { ...initialSendState }; - - default: - return state; - } -} - -// ============================================================================= -// Hook -// ============================================================================= - -export const useSendReducer = createReducerHook(sendReducer, sendActions, initialSendState); -``` - -- [ ] **Step 2: Create the send context** - -Create `src/contexts/send/SendContext.tsx`: - -```typescript -/** - * Send Context - * Manages offchain transfer flow and link generation - */ - -import { createContext, useContext, useCallback, type ReactNode } from 'react'; -import { AztecAddress } from '@aztec/aztec.js/addresses'; -import { useContracts } from '../contracts'; -import { useWallet } from '../wallet'; -import { useNetwork } from '../network'; -import { useSendReducer, type SendState } from './reducer'; -import { encodeTransferLink, type TransferLink } from '../../services/offchainLinkService'; -import { addSentTransfer } from '../../services/sentHistoryService'; -import { executeTransferOffchain } from '../../services/contractService'; - -interface SendContextType extends SendState { - canSend: boolean; - setToken: (token: 'gc' | 'gcp') => void; - setRecipientAddress: (address: string) => void; - setAmount: (amount: string) => void; - executeSend: () => Promise; - dismissError: () => void; - reset: () => void; -} - -const SendContext = createContext(undefined); - -export function useSend() { - const context = useContext(SendContext); - if (context === undefined) { - throw new Error('useSend must be used within a SendProvider'); - } - return context; -} - -interface SendProviderProps { - children: ReactNode; -} - -export function SendProvider({ children }: SendProviderProps) { - const { currentAddress, isUsingEmbeddedWallet } = useWallet(); - const { isLoadingContracts } = useContracts(); - const { activeNetwork } = useNetwork(); - const [state, actions] = useSendReducer(); - - const canSend = - !!state.amount && - parseFloat(state.amount) > 0 && - !!state.recipientAddress && - !isLoadingContracts && - !isUsingEmbeddedWallet && - !!currentAddress; - - const executeSend = useCallback(async () => { - if (!currentAddress || !state.recipientAddress || !state.amount) { - actions.sendError('Missing required fields'); - return; - } - - actions.startSend(); - - try { - const recipient = AztecAddress.fromString(state.recipientAddress); - const amount = BigInt(Math.round(parseFloat(state.amount))); - - const tokenKey = state.token === 'gc' ? 'gregoCoin' : 'gregoCoinPremium'; - const contractAddress = activeNetwork.contracts[tokenKey]; - - const { receipt, offchainMessages } = await executeTransferOffchain( - tokenKey, - currentAddress, - recipient, - amount, - ); - - actions.generatingLink(); - - // Encode the first recipient message into a link - const recipientMessage = offchainMessages[0]; - if (!recipientMessage) { - throw new Error('No offchain message generated for recipient'); - } - - const linkData: TransferLink = { - token: state.token, - amount: state.amount, - recipient: state.recipientAddress, - contractAddress, - txHash: receipt.txHash.toString(), - anchorBlockTimestamp: recipientMessage.anchorBlockTimestamp.toString(), - payload: recipientMessage.payload.map((f: { toString: () => string }) => f.toString()), - }; - - const link = encodeTransferLink(linkData); - actions.linkReady(link); - - // Save to history - addSentTransfer(currentAddress.toString(), { - id: receipt.txHash.toString(), - token: state.token, - amount: state.amount, - recipient: state.recipientAddress, - link, - createdAt: Date.now(), - status: 'confirmed', - }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Send failed. Please try again.'; - actions.sendError(message); - } - }, [currentAddress, state.recipientAddress, state.amount, state.token, activeNetwork, actions]); - - const value: SendContextType = { - ...state, - canSend, - setToken: actions.setToken, - setRecipientAddress: actions.setRecipientAddress, - setAmount: actions.setAmount, - executeSend, - dismissError: actions.dismissError, - reset: actions.reset, - }; - - return {children}; -} -``` - -Note: The `executeTransferOffchain` function referenced above will be added to `contractService.ts` in Task 5. - -- [ ] **Step 3: Create index exports** - -Create `src/contexts/send/index.ts`: - -```typescript -export { SendProvider, useSend } from './SendContext'; -export type { SendPhase, SendState } from './reducer'; -``` - -- [ ] **Step 4: Add SendProvider to main.tsx** - -In `src/main.tsx`, add the import and wrap `` with `SendProvider` as a sibling to `SwapProvider`: - -```typescript -import { StrictMode } from 'react'; -import { createRoot } from 'react-dom/client'; -import { App } from './App.tsx'; -import { NetworkProvider } from './contexts/network/NetworkContext'; -import { WalletProvider } from './contexts/wallet/WalletContext'; -import { ContractsProvider } from './contexts/contracts/ContractsContext'; -import { SwapProvider } from './contexts/swap/SwapContext'; -import { SendProvider } from './contexts/send/SendContext'; -import { OnboardingProvider } from './contexts/onboarding/OnboardingContext'; - -createRoot(document.getElementById('root')!).render( - - - - - - - - - - - - - - - , -); -``` - -- [ ] **Step 5: Commit** - -```bash -git add src/contexts/send/ src/main.tsx -git commit -m "feat: add Send context with reducer, provider, and state machine" -``` - ---- - -### Task 5: Contract Service — executeTransferOffchain - -**Files:** -- Modify: `src/services/contractService.ts` - -This task adds the `executeTransferOffchain` function to the existing contract service. It needs to: -1. Call `transfer_offchain` on the token contract -2. Self-deliver the sender's change note via `offchain_receive` -3. Return the recipient's offchain messages for link encoding - -- [ ] **Step 1: Add the import for OffchainMessage** - -At the top of `src/services/contractService.ts`, add the import for the offchain message type. Check the exact import path from: -- `@aztec/aztec.js/contracts` exports `extractOffchainOutput` -- The `OffchainMessage` type comes from `@aztec/aztec.js/contracts` or `@aztec/aztec.js` - -Add alongside existing imports: - -```typescript -import type { OffchainMessage } from '@aztec/aztec.js/contracts'; -``` - -- [ ] **Step 2: Add executeTransferOffchain function** - -Add this function to `src/services/contractService.ts` after the existing `executeDrip` function: - -```typescript -/** - * Execute an offchain token transfer. - * Sends tokens privately with offchain note delivery, self-delivers the sender's - * change note, and returns the recipient's offchain messages for link encoding. - */ -export async function executeTransferOffchain( - tokenKey: 'gregoCoin' | 'gregoCoinPremium', - fromAddress: AztecAddress, - recipient: AztecAddress, - amount: bigint, - contracts: SwapContracts, -): Promise<{ receipt: TxReceipt; offchainMessages: OffchainMessage[] }> { - const token = tokenKey === 'gregoCoin' ? contracts.gregoCoin : contracts.gregoCoinPremium; - - // 1. Send the offchain transfer transaction - const { receipt, offchainMessages } = await token.methods - .transfer_offchain(recipient, amount) - .send({ from: fromAddress }); - - // 2. Self-deliver sender's change note (manual until F-324 lands) - const senderMessages = offchainMessages.filter( - (msg: OffchainMessage) => msg.recipient.equals(fromAddress), - ); - if (senderMessages.length > 0) { - await token.methods - .offchain_receive( - senderMessages.map((msg: OffchainMessage) => ({ - ciphertext: msg.payload, - recipient: fromAddress, - tx_hash: receipt.txHash.hash, - anchor_block_timestamp: msg.anchorBlockTimestamp, - })), - ) - .simulate({ from: fromAddress }); - } - - // 3. Filter and return recipient's messages for link encoding - const recipientMessages = offchainMessages.filter( - (msg: OffchainMessage) => msg.recipient.equals(recipient), - ); - - return { receipt, offchainMessages: recipientMessages }; -} -``` - -Note: The exact types for `offchainMessages` returned by `.send()` and the shape of the `offchain_receive` argument may need adjustment during implementation based on the SDK version. Check the `TxSendResultMined` type and `offchain_receive` ABI in the compiled contract artifacts. - -- [ ] **Step 3: Add parseSendError function** - -Add this after the existing `parseDripError` function: - -```typescript -export function parseSendError(error: unknown): string { - if (!(error instanceof Error)) return 'Send failed. Please try again.'; - const msg = error.message; - if (msg.includes('Balance too low')) return 'Insufficient token balance'; - if (msg.includes('User denied') || msg.includes('rejected')) return 'Transaction was rejected in wallet'; - if (msg.includes('invalid') && msg.includes('address')) return 'Invalid recipient address'; - return msg; -} -``` - -- [ ] **Step 4: Commit** - -```bash -git add src/services/contractService.ts -git commit -m "feat: add executeTransferOffchain to contract service - -Handles offchain transfer execution, sender change note self-delivery, -and recipient message extraction for link encoding." -``` - ---- - -### Task 6: Install QR Code Dependency - -**Files:** -- Modify: `package.json` - -- [ ] **Step 1: Install qrcode.react** - -```bash -yarn add qrcode.react -``` - -- [ ] **Step 2: Commit** - -```bash -git add package.json yarn.lock -git commit -m "chore: add qrcode.react dependency for transfer link QR codes" -``` - ---- - -### Task 7: Send UI Components - -**Files:** -- Create: `src/components/send/SendForm.tsx` -- Create: `src/components/send/SendProgress.tsx` -- Create: `src/components/send/LinkDisplay.tsx` -- Create: `src/components/send/SentHistory.tsx` -- Create: `src/components/send/SendContainer.tsx` - -- [ ] **Step 1: Create SendForm** - -Create `src/components/send/SendForm.tsx`: - -```tsx -import { Box, TextField, Typography, ToggleButton, ToggleButtonGroup, Button } from '@mui/material'; -import { useSend } from '../../contexts/send'; - -interface SendFormProps { - balance: { gc: bigint | null; gcp: bigint | null }; -} - -export function SendForm({ balance }: SendFormProps) { - const { token, recipientAddress, amount, canSend, setToken, setRecipientAddress, setAmount, executeSend, phase } = - useSend(); - - const isSending = phase === 'sending' || phase === 'generating_link'; - const currentBalance = token === 'gc' ? balance.gc : balance.gcp; - - return ( - - {/* Token Selector */} - - - Token - - value && setToken(value)} - size="small" - fullWidth - disabled={isSending} - > - GregoCoin - GregoCoinPremium - - - - {/* Recipient Address */} - setRecipientAddress(e.target.value)} - fullWidth - disabled={isSending} - size="small" - /> - - {/* Amount */} - - setAmount(e.target.value)} - fullWidth - disabled={isSending} - size="small" - slotProps={{ - input: { - endAdornment: currentBalance !== null ? ( - - Balance: {currentBalance.toString()} - - ) : null, - }, - }} - /> - - - {/* Send Button */} - - - ); -} -``` - -- [ ] **Step 2: Create SendProgress** - -Create `src/components/send/SendProgress.tsx`: - -```tsx -import { Box, Typography, CircularProgress } from '@mui/material'; -import type { SendPhase } from '../../contexts/send'; - -interface SendProgressProps { - phase: SendPhase; -} - -const phaseMessages: Record = { - sending: 'Sending transaction...', - generating_link: 'Generating claim link...', -}; - -export function SendProgress({ phase }: SendProgressProps) { - const message = phaseMessages[phase]; - if (!message) return null; - - return ( - - - - {message} - - - ); -} -``` - -- [ ] **Step 3: Create LinkDisplay** - -Create `src/components/send/LinkDisplay.tsx`: - -```tsx -import { Box, Typography, Button, IconButton, Snackbar } from '@mui/material'; -import ContentCopyIcon from '@mui/icons-material/ContentCopy'; -import { QRCodeSVG } from 'qrcode.react'; -import { useState } from 'react'; - -interface LinkDisplayProps { - link: string; - amount: string; - token: 'gc' | 'gcp'; - recipient: string; - onReset: () => void; -} - -export function LinkDisplay({ link, amount, token, recipient, onReset }: LinkDisplayProps) { - const [copied, setCopied] = useState(false); - const tokenName = token === 'gc' ? 'GregoCoin' : 'GregoCoinPremium'; - - const handleCopy = async () => { - await navigator.clipboard.writeText(link); - setCopied(true); - }; - - return ( - - - Sent! - - - {amount} {tokenName} → {recipient.slice(0, 8)}...{recipient.slice(-4)} - - - {/* Copyable Link */} - - - {link} - - - - - - - {/* QR Code */} - - - - - Scan to claim - - - - - setCopied(false)} - message="Link copied!" - /> - - ); -} -``` - -- [ ] **Step 4: Create SentHistory** - -Create `src/components/send/SentHistory.tsx`: - -```tsx -import { Box, Typography, IconButton, Collapse, Snackbar, Chip } from '@mui/material'; -import ContentCopyIcon from '@mui/icons-material/ContentCopy'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import { useState } from 'react'; -import { getSentTransfers, type SentTransfer } from '../../services/sentHistoryService'; - -interface SentHistoryProps { - senderAddress: string; -} - -function timeAgo(timestamp: number): string { - const seconds = Math.floor((Date.now() - timestamp) / 1000); - if (seconds < 60) return `${seconds}s ago`; - const minutes = Math.floor(seconds / 60); - if (minutes < 60) return `${minutes}m ago`; - const hours = Math.floor(minutes / 60); - if (hours < 24) return `${hours}h ago`; - return `${Math.floor(hours / 24)}d ago`; -} - -function StatusChip({ status }: { status: SentTransfer['status'] }) { - if (status === 'confirmed') return null; - const color = status === 'pending' ? 'warning' : 'error'; - return ; -} - -export function SentHistory({ senderAddress }: SentHistoryProps) { - const [copied, setCopied] = useState(false); - const [expanded, setExpanded] = useState(false); - const transfers = getSentTransfers(senderAddress); - - if (transfers.length === 0) return null; - - const visibleTransfers = expanded ? transfers : transfers.slice(0, 3); - const hasMore = transfers.length > 3; - - const handleCopy = async (link: string) => { - await navigator.clipboard.writeText(link); - setCopied(true); - }; - - return ( - - - Sent transfers - - {visibleTransfers.map(transfer => ( - - - - {transfer.amount} {transfer.token === 'gc' ? 'GC' : 'GCP'} - - - → {transfer.recipient.slice(0, 8)}...{transfer.recipient.slice(-4)} - - - - - - {timeAgo(transfer.createdAt)} - - handleCopy(transfer.link)}> - - - - - ))} - {hasMore && ( - - setExpanded(!expanded)} - sx={{ transform: expanded ? 'rotate(180deg)' : 'none', transition: '0.2s' }} - > - - - - )} - setCopied(false)} message="Link copied!" /> - - ); -} -``` - -- [ ] **Step 5: Create SendContainer** - -Create `src/components/send/SendContainer.tsx`. This orchestrates the send flow, similar to how `SwapContainer` orchestrates swaps: - -```tsx -import { Box, Alert } from '@mui/material'; -import { useSend } from '../../contexts/send'; -import { useWallet } from '../../contexts/wallet'; -import { useContracts } from '../../contexts/contracts'; -import { SendForm } from './SendForm'; -import { SendProgress } from './SendProgress'; -import { LinkDisplay } from './LinkDisplay'; -import { SentHistory } from './SentHistory'; -import { useEffect, useState } from 'react'; - -export function SendContainer() { - const { phase, error, generatedLink, token, amount, recipientAddress, dismissError, reset } = useSend(); - const { currentAddress, isUsingEmbeddedWallet } = useWallet(); - const { fetchBalances } = useContracts(); - const [balances, setBalances] = useState<{ gc: bigint | null; gcp: bigint | null }>({ - gc: null, - gcp: null, - }); - - useEffect(() => { - if (currentAddress && !isUsingEmbeddedWallet) { - fetchBalances().then(([gc, gcp]) => setBalances({ gc, gcp })); - } - }, [currentAddress, isUsingEmbeddedWallet, fetchBalances]); - - // Refresh balances after a successful send - useEffect(() => { - if (phase === 'link_ready' && currentAddress) { - fetchBalances().then(([gc, gcp]) => setBalances({ gc, gcp })); - } - }, [phase, currentAddress, fetchBalances]); - - if (isUsingEmbeddedWallet) { - return ( - - Connect an external wallet to send tokens. - - ); - } - - return ( - - {phase === 'link_ready' && generatedLink ? ( - - ) : ( - <> - - - - )} - - {error && ( - - {error} - - )} - - {currentAddress && } - - ); -} -``` - -- [ ] **Step 6: Commit** - -```bash -git add src/components/send/ -git commit -m "feat: add Send UI components - -SendForm (token selector, address, amount), SendProgress, LinkDisplay -(copyable link + QR code), SentHistory, and SendContainer orchestrator." -``` - ---- - -### Task 8: Claim Page Components - -**Files:** -- Create: `src/components/claim/ClaimProgress.tsx` -- Create: `src/components/claim/ClaimSuccess.tsx` -- Create: `src/components/claim/ClaimPage.tsx` - -- [ ] **Step 1: Create ClaimProgress** - -Create `src/components/claim/ClaimProgress.tsx`: - -```tsx -import { Box, Typography, CircularProgress } from '@mui/material'; - -type ClaimPhase = 'claiming' | 'verifying'; - -interface ClaimProgressProps { - phase: ClaimPhase; -} - -const phaseMessages: Record = { - claiming: 'Claiming tokens...', - verifying: 'Verifying amount...', -}; - -export function ClaimProgress({ phase }: ClaimProgressProps) { - return ( - - - - {phaseMessages[phase]} - - - ); -} -``` - -- [ ] **Step 2: Create ClaimSuccess** - -Create `src/components/claim/ClaimSuccess.tsx`: - -```tsx -import { Box, Typography, Button, Chip } from '@mui/material'; -import CheckCircleIcon from '@mui/icons-material/CheckCircle'; - -interface ClaimSuccessProps { - amount: string; - tokenName: string; - verified: boolean; - onGoToSwap: () => void; -} - -export function ClaimSuccess({ amount, tokenName, verified, onGoToSwap }: ClaimSuccessProps) { - return ( - - - - Tokens Claimed! - - - - {amount} {tokenName} - - - - - - ); -} -``` - -- [ ] **Step 3: Create ClaimPage** - -Create `src/components/claim/ClaimPage.tsx`. This is the main claim flow orchestrator: - -```tsx -import { Box, Typography, Button, Alert, CircularProgress, Container, Chip } from '@mui/material'; -import { useEffect, useState, useCallback } from 'react'; -import { useWallet } from '../../contexts/wallet'; -import { useContracts } from '../../contexts/contracts'; -import { extractClaimPayload, type TransferLink } from '../../services/offchainLinkService'; -import { ClaimProgress } from './ClaimProgress'; -import { ClaimSuccess } from './ClaimSuccess'; -import { GregoSwapLogo } from '../GregoSwapLogo'; - -type ClaimState = - | { phase: 'decoding' } - | { phase: 'preview'; data: TransferLink } - | { phase: 'claiming'; data: TransferLink } - | { phase: 'verifying'; data: TransferLink } - | { phase: 'claimed'; data: TransferLink; verified: boolean } - | { phase: 'error'; message: string }; - -export function ClaimPage() { - const [state, setState] = useState({ phase: 'decoding' }); - const { wallet, currentAddress, isUsingEmbeddedWallet } = useWallet(); - const { fetchBalances, registerBaseContracts, isLoadingContracts } = useContracts(); - - // Step 1: Decode the link on mount - useEffect(() => { - const data = extractClaimPayload(); - if (!data) { - setState({ phase: 'error', message: 'Invalid or missing claim link.' }); - return; - } - setState({ phase: 'preview', data }); - }, []); - - // Step 2: Execute the claim - const doClaim = useCallback(async () => { - if (state.phase !== 'preview') return; - const { data } = state; - - setState({ phase: 'claiming', data }); - - try { - // Ensure contracts are registered - if (!isLoadingContracts && wallet) { - await registerBaseContracts(); - } - - // Wait for wallet to be ready - if (!wallet || !currentAddress) { - // Wallet should auto-create (embedded) or already be connected - setState({ phase: 'error', message: 'No wallet available. Please refresh and try again.' }); - return; - } - - // Get balance before claim (for verification diff) - let balanceBefore = 0n; - try { - const [gc, gcp] = await fetchBalances(); - balanceBefore = data.token === 'gc' ? gc : gcp; - } catch { - // New wallet might not have balance yet — that's fine - } - - // Reconstruct Fr values from payload strings - const { Fr } = await import('@aztec/aztec.js/fields'); - const payload = data.payload.map((s: string) => Fr.fromString(s)); - - // Call offchain_receive on the token contract - const tokenKey = data.token === 'gc' ? 'gregoCoin' : 'gregoCoinPremium'; - // Access the token contract from the contracts context - // Note: this will need to be adapted based on how contracts are exposed - const { AztecAddress } = await import('@aztec/aztec.js/addresses'); - const recipient = AztecAddress.fromString(data.recipient); - - // The actual offchain_receive call — this needs the token contract instance. - // The ContractsContext will need to expose the token contracts or a claimOffchain method. - // For now, show the pattern: - // await tokenContract.methods.offchain_receive([{ - // ciphertext: payload, - // recipient, - // tx_hash: data.txHash, - // anchor_block_timestamp: BigInt(data.anchorBlockTimestamp), - // }]).simulate({ from: currentAddress }); - - setState({ phase: 'verifying', data }); - - // Verify: check balance after - const [gcAfter, gcpAfter] = await fetchBalances(); - const balanceAfter = data.token === 'gc' ? gcAfter : gcpAfter; - const received = balanceAfter - balanceBefore; - const expectedAmount = BigInt(Math.round(parseFloat(data.amount))); - const verified = received >= expectedAmount; - - setState({ phase: 'claimed', data, verified }); - } catch (error) { - const message = error instanceof Error ? error.message : 'Claim failed. Please try again.'; - setState({ phase: 'error', message }); - } - }, [state, wallet, currentAddress, isLoadingContracts, registerBaseContracts, fetchBalances]); - - const handleGoToSwap = () => { - window.location.hash = ''; - window.location.reload(); - }; - - const tokenName = (t: string) => (t === 'gc' ? 'GregoCoin' : 'GregoCoinPremium'); - - return ( - - - - - - - {state.phase === 'decoding' && ( - - - - )} - - {state.phase === 'preview' && ( - - - Someone sent you - - - - {state.data.amount} {tokenName(state.data.token)} - - - - - - )} - - {state.phase === 'claiming' && } - {state.phase === 'verifying' && } - - {state.phase === 'claimed' && ( - - )} - - {state.phase === 'error' && ( - {state.message} - )} - - - ); -} -``` - -**Implementation note:** The `offchain_receive` call in `ClaimPage` is commented as a pattern because the `ContractsContext` doesn't currently expose raw token contract instances. During implementation, either: -1. Expose the token contracts from `ContractsContext` (add a `getTokenContract(tokenKey)` method), or -2. Add a `claimOffchainTransfer(tokenKey, message)` method to `ContractsContext` - -Option 2 is cleaner — it follows the existing pattern where `ContractsContext` wraps contract interactions. - -- [ ] **Step 4: Commit** - -```bash -git add src/components/claim/ -git commit -m "feat: add Claim page components - -ClaimPage (orchestrator with state machine), ClaimProgress, -and ClaimSuccess. Handles link decoding, wallet resolution, -offchain_receive, and balance verification." -``` - ---- - -### Task 9: App Routing and Tab Bar - -**Files:** -- Modify: `src/components/App.tsx` - -- [ ] **Step 1: Add route detection and tab bar to App.tsx** - -Update `src/components/App.tsx` to: -1. Detect `/#/claim/` routes and render `ClaimPage` instead of the main UI -2. Add a Swap/Send tab bar - -Replace the entire file with: - -```tsx -import { ThemeProvider, CssBaseline, Container, Box, Typography, Tabs, Tab } from '@mui/material'; -import { theme } from './theme'; -import { GregoSwapLogo } from './components/GregoSwapLogo'; -import { WalletChip } from './components/WalletChip'; -import { NetworkSwitcher } from './components/NetworkSwitcher'; -import { FooterInfo } from './components/FooterInfo'; -import { SwapContainer } from './components/swap'; -import { SendContainer } from './components/send/SendContainer'; -import { ClaimPage } from './components/claim/ClaimPage'; -import { useWallet } from './contexts/wallet'; -import { useOnboarding } from './contexts/onboarding'; -import { OnboardingModal } from './components/OnboardingModal'; -import { TxNotificationCenter } from './components/TxNotificationCenter'; -import { isClaimRoute } from './services/offchainLinkService'; -import type { AztecAddress } from '@aztec/aztec.js/addresses'; -import { useState } from 'react'; - -export function App() { - const { disconnectWallet, setCurrentAddress, currentAddress, error: walletError, isLoading: walletLoading } = - useWallet(); - const { isOnboardingModalOpen, startOnboarding, resetOnboarding, status: onboardingStatus } = useOnboarding(); - const [activeTab, setActiveTab] = useState(0); - - const isOnboarded = onboardingStatus === 'completed'; - - // If on a claim route, render the claim page directly - if (isClaimRoute()) { - return ( - - - - - - - ); - } - - const handleWalletClick = () => { - if (isOnboarded && currentAddress) { - resetOnboarding(); - } - startOnboarding(); - }; - - const handleDisconnect = async () => { - await disconnectWallet(); - resetOnboarding(); - }; - - return ( - - - - - - - - - - - - - - Swap GregoCoin for GregoCoinPremium - - - - {/* Tab Bar */} - setActiveTab(value)} - centered - sx={{ - mb: 3, - '& .MuiTab-root': { color: 'text.secondary', fontWeight: 600 }, - '& .Mui-selected': { color: 'primary.main' }, - '& .MuiTabs-indicator': { backgroundColor: 'primary.main' }, - }} - > - - - - - {/* Tab Content */} - {activeTab === 0 && } - {activeTab === 1 && } - - {walletError && ( - - - - Wallet Connection Error - - - {walletError} - - - - )} - - {walletLoading && !walletError && ( - - - - Connecting to network... - - - - )} - - - - - - { - setCurrentAddress(address); - }} - /> - - - - ); -} -``` - -- [ ] **Step 2: Verify build compiles** - -```bash -yarn build -``` - -Expected: Build succeeds (or only type errors from the `offchain_receive` integration in ClaimPage, which will be finalized during integration testing). - -- [ ] **Step 3: Commit** - -```bash -git add src/components/App.tsx -git commit -m "feat: add hash routing for claim page and Swap/Send tab bar - -Detects /#/claim/ routes and renders ClaimPage. Adds Swap/Send -tabs to the main interface." -``` - ---- - -### Task 10: Integration — Wire ContractsContext for Offchain Transfers - -**Files:** -- Modify: `src/contexts/contracts/ContractsContext.tsx` - -The `ClaimPage` and `SendContext` need to interact with token contracts for `transfer_offchain` and `offchain_receive`. The cleanest approach is to add methods to `ContractsContext`. - -- [ ] **Step 1: Add sendOffchain and claimOffchainTransfer to ContractsContextType** - -In `src/contexts/contracts/ContractsContext.tsx`, add to the `ContractsContextType` interface: - -```typescript - // Offchain transfer methods - sendOffchain: (tokenKey: 'gregoCoin' | 'gregoCoinPremium', recipient: AztecAddress, amount: bigint) => Promise<{ receipt: TxReceipt; offchainMessages: any[] }>; - claimOffchainTransfer: (tokenKey: 'gregoCoin' | 'gregoCoinPremium', message: { ciphertext: any[]; recipient: AztecAddress; tx_hash: string; anchor_block_timestamp: bigint }) => Promise; -``` - -- [ ] **Step 2: Implement the methods in ContractsProvider** - -Add the implementations inside `ContractsProvider`, following the pattern of existing methods like `swap`: - -```typescript - const sendOffchain = useCallback(async ( - tokenKey: 'gregoCoin' | 'gregoCoinPremium', - recipient: AztecAddress, - amount: bigint, - ) => { - if (!wallet || !currentAddress || !state.contracts) { - throw new Error('Contracts not initialized'); - } - return contractService.executeTransferOffchain( - tokenKey, - currentAddress, - recipient, - amount, - state.contracts, - ); - }, [wallet, currentAddress, state.contracts]); - - const claimOffchainTransfer = useCallback(async ( - tokenKey: 'gregoCoin' | 'gregoCoinPremium', - message: { ciphertext: any[]; recipient: AztecAddress; tx_hash: string; anchor_block_timestamp: bigint }, - ) => { - if (!wallet || !currentAddress || !state.contracts) { - throw new Error('Contracts not initialized'); - } - const token = tokenKey === 'gregoCoin' ? state.contracts.gregoCoin : state.contracts.gregoCoinPremium; - await token.methods - .offchain_receive([message]) - .simulate({ from: currentAddress }); - }, [wallet, currentAddress, state.contracts]); -``` - -Add both methods to the context value object. - -- [ ] **Step 3: Update SendContext to use sendOffchain from ContractsContext** - -In `src/contexts/send/SendContext.tsx`, replace the direct `executeTransferOffchain` call with: - -```typescript -const { sendOffchain } = useContracts(); - -// Inside executeSend: -const { receipt, offchainMessages } = await sendOffchain(tokenKey, recipient, amount); -``` - -Remove the direct import of `executeTransferOffchain` from `contractService`. - -- [ ] **Step 4: Update ClaimPage to use claimOffchainTransfer** - -In `src/components/claim/ClaimPage.tsx`, replace the commented-out `offchain_receive` call with: - -```typescript -const { claimOffchainTransfer, registerBaseContracts, fetchBalances } = useContracts(); - -// Inside doClaim: -const { Fr } = await import('@aztec/aztec.js/fields'); -const { AztecAddress } = await import('@aztec/aztec.js/addresses'); - -await claimOffchainTransfer( - data.token === 'gc' ? 'gregoCoin' : 'gregoCoinPremium', - { - ciphertext: data.payload.map((s: string) => Fr.fromString(s)), - recipient: AztecAddress.fromString(data.recipient), - tx_hash: data.txHash, - anchor_block_timestamp: BigInt(data.anchorBlockTimestamp), - }, -); -``` - -- [ ] **Step 5: Commit** - -```bash -git add src/contexts/contracts/ContractsContext.tsx src/contexts/send/SendContext.tsx src/components/claim/ClaimPage.tsx -git commit -m "feat: wire offchain transfer methods through ContractsContext - -Add sendOffchain and claimOffchainTransfer to ContractsContext. -Update SendContext and ClaimPage to use them." -``` - ---- - -### Task 11: Deploy and End-to-End Test - -**Files:** No new files — integration testing - -- [ ] **Step 1: Deploy contracts to local sandbox** - -```bash -# Terminal 1: Start Aztec sandbox (if not running) -aztec start --local-network - -# Terminal 2: Deploy contracts with the forked token -PASSWORD=test123 yarn deploy:local -``` - -Expected: All contracts deploy successfully, including the forked Token contract with `transfer_offchain`. - -- [ ] **Step 2: Start dev server and test the send flow** - -```bash -yarn serve -``` - -1. Open the app, connect an external wallet, complete onboarding -2. Switch to the "Send" tab -3. Select GregoCoin, enter recipient address (use a second account), enter amount -4. Click "Send & Generate Link" -5. Verify: Transaction sends, link is generated, QR code appears -6. Copy the link - -- [ ] **Step 3: Test the claim flow** - -1. Open the copied link in a new browser tab/incognito window -2. Verify: Preview shows "Someone sent you X GregoCoin" with "unverified" badge -3. Click "Claim" -4. Verify: Wallet auto-creates, offchain_receive is called, balance is verified -5. Verify: Success screen with verified amount - -- [ ] **Step 4: Test sent history** - -1. Go back to the sender's tab, check the Send tab -2. Verify: Sent history shows the transfer with "confirmed" status -3. Click "Copy link" — verify it copies the same link - -- [ ] **Step 5: Test error cases** - -1. Try opening a claim link with a wallet whose address doesn't match the recipient — should show decryption error -2. Try sending with insufficient balance — should show balance error -3. Try claim with an invalid/corrupted link — should show invalid link error - -- [ ] **Step 6: Commit any fixes from testing** - -```bash -git add -A -git commit -m "fix: integration adjustments from end-to-end testing" -``` - ---- - -### Task 12: Redeploy Contracts (if needed) - -If the compiled Token contract artifact changed (new ABI from `transfer_offchain`), the deploy script may need updating to reference the new artifact. Check: - -- [ ] **Step 1: Verify contract artifacts** - -```bash -ls contracts/target/ -``` - -Check that `Token.json` (or equivalent artifact) includes the `transfer_offchain` method in its ABI. - -- [ ] **Step 2: Update deploy script if needed** - -Check the deploy script (`deploy:local` in package.json) to ensure it deploys the forked Token contract. Since we kept the same contract name (`token_contract`), the artifacts should be compatible. - -- [ ] **Step 3: Update deployed-addresses.json if needed** - -If contract addresses change after redeployment, update `src/config/networks/deployed-addresses.json` with new addresses. - -- [ ] **Step 4: Commit** - -```bash -git add contracts/target/ src/config/networks/ -git commit -m "chore: update contract artifacts and deployed addresses for forked token" -``` diff --git a/docs/superpowers/specs/2026-04-09-p2p-offchain-transfers-design.md b/docs/superpowers/specs/2026-04-09-p2p-offchain-transfers-design.md deleted file mode 100644 index 681bbe3..0000000 --- a/docs/superpowers/specs/2026-04-09-p2p-offchain-transfers-design.md +++ /dev/null @@ -1,373 +0,0 @@ -# P2P Private Transfers with Offchain Delivery - -**Date:** 2026-04-09 -**Status:** Design approved -**Goal:** Add P2P private token transfers to GregoSwap using Aztec's offchain delivery feature, with shareable claim links and QR codes as the delivery channel. - -## Motivation - -This feature serves as a dogfooding vehicle for Aztec's offchain delivery feature. It exercises the full vertical slice: - -- **Contract DX:** Writing `.deliver(MessageDelivery.OFFCHAIN)` in a forked Token contract -- **SDK integration:** Extracting `offchainMessages` from transactions, calling `offchain_receive()` -- **Delivery channel:** Encoding offchain messages into shareable URLs and QR codes -- **End-user experience:** Sender generates a link, recipient opens it and claims tokens - -Lessons learned will be retrofitted into the offchain delivery feature itself. - -## Design Decisions - -| Decision | Choice | Rationale | -|----------|--------|-----------| -| Delivery channel | Shareable link + QR code | Tests URL-based delivery; works for both remote sharing and in-person | -| Token note delivery | Offchain (not escrow) | Exercises the core offchain delivery path; note is created directly for recipient | -| Expiration | None (protocol-level tx expiry only) | Keeps focus on offchain delivery; escrow would bypass it | -| Transferable tokens | GregoCoin and GregoCoinPremium | Both tokens supported via token selector | -| Recipient wallet | Use existing if connected; auto-create embedded if not | Minimum friction for new users | -| Claim trigger | Explicit "Claim" button | Gives recipient time to understand what's happening before committing | -| Amount display | Show URL amount optimistically, verify after claim | Instant preview with trust-but-verify UX | -| Contract change | Local fork of Token contract | Iterate locally; upstream later if it works well | -| Transfer event | Offchain (same as notes) | Fully consistent — nothing onchain except the note hash | - -## Non-Goals - -- No expiration/reclaim mechanism (tokens are gone once the note is in the tree) -- No claim detection from sender's perspective (fire-and-forget by design) -- No address book or ENS-style resolution -- No "send to anyone" pattern (recipient address is required for encryption) - -## Architecture - -### Overview - -``` -Sender Recipient - │ │ - ├─ transfer_offchain(to, amt) │ - │ ├─ subtract sender balance │ - │ ├─ add recipient note ──── .deliver(OFFCHAIN) - │ ├─ add sender change note ─ .deliver(OFFCHAIN) - │ └─ emit Transfer event ──── .deliver(OFFCHAIN) - │ │ - ├─ SDK returns offchainMessages │ - ├─ Self-deliver change note │ - ├─ Encode recipient msg → URL │ - ├─ Show link + QR code │ - │ │ - │ ─── share link ───────────────>│ - │ ├─ Open link → preview amount - │ ├─ Click "Claim" - │ ├─ Connect/create wallet - │ ├─ offchain_receive(message) - │ ├─ PXE syncs → note decrypted - │ ├─ Verify balance - │ └─ Tokens available -``` - -### File Structure - -``` -contracts/ - token/ # NEW — fork of standard Token contract - src/main.nr # standard Token + transfer_offchain method - Nargo.toml - amm/Nargo.toml # MOD — point token dep to local fork - proof_of_password/Nargo.toml # MOD — point token dep to local fork - -src/ - services/ - offchainLinkService.ts # NEW — encode/decode transfer links - sentHistoryService.ts # NEW — localStorage CRUD for sent transfers - contractService.ts # MOD — add executeTransferOffchain - components/ - App.tsx # MOD — route detection, tab bar - send/ - SendContainer.tsx # NEW — orchestrates send flow - SendForm.tsx # NEW — token selector, address, amount - SendProgress.tsx # NEW — sending + generating states - LinkDisplay.tsx # NEW — copyable link + QR code - SentHistory.tsx # NEW — list of sent transfers - claim/ - ClaimPage.tsx # NEW — orchestrates claim flow - ClaimProgress.tsx # NEW — state machine progress - ClaimSuccess.tsx # NEW — success state with CTA - swap/ - SwapContainer.tsx # MOD — wrap in tab structure - contexts/ - send/ - SendContext.tsx # NEW — send flow state management - reducer.ts # NEW — send state machine - index.ts -``` - -## Section 1: Smart Contract - -Fork the standard Token contract into `contracts/token/`. The only addition is a `transfer_offchain` method — identical to `transfer` but with `MessageDelivery.OFFCHAIN` for all deliveries. - -### New method - -```noir -#[external("private")] -fn transfer_offchain(to: AztecAddress, amount: u128) { - let from = self.msg_sender(); - - let change = self.internal.subtract_balance(from, amount, INITIAL_TRANSFER_CALL_MAX_NOTES); - self.storage.balances.at(from).add(change).deliver(MessageDelivery.OFFCHAIN); - self.storage.balances.at(to).add(amount).deliver(MessageDelivery.OFFCHAIN); - - self.emit(Transfer { from, to, amount }).deliver_to( - to, - MessageDelivery.OFFCHAIN, - ); -} -``` - -### What changes vs. standard Token - -- **Add** `transfer_offchain` — new private function (~10 lines) -- **Add** `MessageDelivery` import (if not already present) -- **No changes** to `subtract_balance`, storage, other methods, or recursive balance logic - -### Dependency updates - -Update `Nargo.toml` in `contracts/amm/` and `contracts/proof_of_password/` to point `token` dependency to the local fork: - -```toml -token = { path = "../token" } -``` - -## Section 2: SDK Integration & Link Encoding - -### 2a. Extracting offchain messages - -The SDK's `.send()` already returns `{ receipt, offchainEffects, offchainMessages }` (type `TxSendResultMined`). No extra SDK work needed. - -New function in `contractService.ts`: - -```typescript -async function executeTransferOffchain( - token: TokenContract, - fromAddress: AztecAddress, - recipient: AztecAddress, - amount: bigint, -): Promise<{ receipt: TxReceipt; offchainMessages: OffchainMessage[] }> { - // 1. Send transaction — SDK extracts offchain messages automatically - const { receipt, offchainMessages } = await token.methods - .transfer_offchain(recipient, amount) - .send({ from: fromAddress }); - - // 2. Self-deliver sender's change note - const senderMessages = offchainMessages - .filter(msg => msg.recipient.equals(fromAddress)); - if (senderMessages.length > 0) { - await token.methods - .offchain_receive(senderMessages.map(msg => ({ - ciphertext: msg.payload, - recipient: fromAddress, - tx_hash: receipt.txHash.hash, - anchor_block_timestamp: msg.anchorBlockTimestamp, - }))) - .simulate({ from: fromAddress }); - } - - // 3. Return recipient's messages for link encoding - const recipientMessages = offchainMessages - .filter(msg => msg.recipient.equals(recipient)); - - return { receipt, offchainMessages: recipientMessages }; -} -``` - -### 2b. Link encoding - -New file: `src/services/offchainLinkService.ts` - -```typescript -interface TransferLink { - token: 'gc' | 'gcp'; // which token - amount: string; // human-readable amount (untrusted, for preview) - recipient: string; // intended recipient Aztec address - contractAddress: string; // token contract address - txHash: string; // originating tx hash - anchorBlockTimestamp: string; // for offchain_receive - payload: string[]; // Fr[] as hex strings (encrypted note ciphertext) -} - -function encodeTransferLink(data: TransferLink): string { - const json = JSON.stringify(data); - const encoded = btoa(json) - .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); - return `${window.location.origin}/#/claim/${encoded}`; -} - -function decodeTransferLink(encoded: string): TransferLink { - const base64 = encoded.replace(/-/g, '+').replace(/_/g, '/'); - return JSON.parse(atob(base64)); -} -``` - -### 2c. URL size estimate - -| Component | Size (approx) | -|-----------|---------------| -| Payload (Fr[] encrypted ciphertext) | ~20 fields x 64 hex chars = ~1,280 chars | -| Metadata (token, amount, addresses, tx hash) | ~300 chars | -| Base64 overhead (~33%) | ~520 chars | -| **Total URL length** | **~2,100 chars** | - -Within limits for browsers (~8,000 chars) and QR codes (~4,296 alphanumeric chars). - -## Section 3: Claim Flow (Recipient UX) - -### Route - -New hash route: `/#/claim/{base64url_payload}` - -`App.tsx` detects this route and renders `ClaimPage` instead of the swap interface. - -### State machine - -``` -decoding → preview → claiming → verifying → claimed - ↗ - error (from any state) -``` - -| State | What happens | User sees | -|-------|-------------|-----------| -| `decoding` | Parse base64url from URL, validate structure | Brief loading flash | -| `preview` | Display transfer info from URL metadata | "Someone sent you 50 GregoCoin!" with amount and token. **"Claim" button** | -| `claiming` | User clicked Claim. Connect/create wallet, register token contract, call `offchain_receive()` | "Claiming tokens..." spinner | -| `verifying` | Query `balance_of_private()`, compare to expected amount | "Verifying amount..." indicator (amount badge updates) | -| `claimed` | Balance confirmed | Amount badge turns green, "Tokens claimed! Start swapping" CTA | -| `error` | Invalid link, network error, amount mismatch, decryption failure | Error message with description | - -### Wallet resolution - -``` -On "Claim" button press: -├── External wallet already connected? -│ └── YES → Use it. Register token contract if needed. -│ -└── No wallet connected? - └── Auto-create embedded wallet - → Create node client - → Create embedded wallet - → Register token contract - → Claim into embedded wallet's address -``` - -The link's `recipient` field must match the claiming wallet's address — `offchain_receive` will fail to decrypt if they don't match. This surfaces as a clear error. - -### Amount verification - -The claim page shows the amount from the URL immediately (optimistic preview). After `offchain_receive` completes, it queries the balance and confirms the amount matches. The amount badge transitions from "unverified" to "verified" state. - -For new users (fresh embedded wallet): balance = received amount. -For returning users: snapshot balance before claim, diff after. - -### After claiming - -Success screen with CTA to navigate to the main swap page. If using an auto-created embedded wallet, the user is considered onboarded — skip the normal onboarding flow. - -## Section 4: Sender UX - -### Navigation - -Add a **Swap / Send** tab bar above the current swap interface. Both tabs share the same container width and visual style. The Send tab is only enabled when the user has a connected external wallet with a balance. - -### Send form - -- **Token toggle:** GregoCoin / GregoCoinPremium (two buttons, not a dropdown) -- **Recipient address:** Text input accepting a full Aztec address (hex string) -- **Amount:** Number input with balance display -- **Button:** "Send & Generate Link" - -### Send flow state machine - -``` -idle → sending → generating_link → link_ready -``` - -| State | What happens | -|-------|-------------| -| `idle` | Form visible, user fills in token + recipient + amount | -| `sending` | Call `transfer_offchain()`, wait for tx receipt + offchain messages | -| `generating_link` | Self-deliver change note, filter recipient messages, encode URL, generate QR | -| `link_ready` | Show copyable link + QR code, save to sent history | - -### Link display - -After generation: -- Copyable link field with copy button -- QR code (using `qrcode.react` or similar lightweight library — new dependency) -- "Send another" button to reset the form - -## Section 5: Sent History - -### Location - -Below the Send form, visible only on the Send tab. Collapsed by default if more than 3 entries. - -### Data model - -```typescript -interface SentTransfer { - id: string; // tx hash - token: 'gc' | 'gcp'; - amount: string; - recipient: string; // Aztec address - link: string; // full claim URL - createdAt: number; // timestamp - status: 'pending' | 'confirmed' | 'expired'; -} -``` - -Stored in localStorage keyed by sender address: `gregoswap_sent_transfers_{senderAddress}` - -### Status tracking - -Aztec transactions have a protocol-level 24-hour mining window. The tx hash is derived from the expiration timestamp, so we can determine the deadline without extra queries. - -| Status | Meaning | -|--------|---------| -| `pending` | Tx sent but not yet confirmed as mined | -| `confirmed` | Tx mined, note exists in the tree, link is valid and claimable | -| `expired` | 24h passed without the tx being mined (reorg, network issues, etc.) — tokens were never sent | - -The happy path is always `pending → confirmed` (typically within seconds/minutes). The `expired` state is an edge case (reorgs, sequencer issues) but important for informing the user that their tokens weren't actually sent. - -**Status resolution:** When the Send tab loads, pending transfers are checked by querying the node for the tx receipt (using the stored tx hash). If the tx is mined, status updates to `confirmed`. If the current time exceeds the 24h deadline derived from the tx hash, status updates to `expired`. - -**Claim detection is not possible** with the direct transfer approach. Once the tx is mined, the sender has no on-chain way to know if the recipient called `offchain_receive`. The history serves as a "links I've generated" log with re-share capability, not a live status tracker. - -### List UI - -Each row shows: -- Token amount and type (e.g., "50 GC") -- Truncated recipient address -- Relative timestamp -- Status indicator (for pending/expired states) -- "Copy link" button for re-sharing - -## New Dependencies - -- `qrcode.react` (or similar) — QR code generation for claim links - -## Dogfooding Observations (Captured During Design) - -These are insights surfaced during the design process, worth feeding back into the offchain delivery feature: - -1. **Partial notes don't support offchain delivery.** The `partial_note.complete()` flow hardcodes `ONCHAIN_UNCONSTRAINED`. This means most DeFi contracts (AMMs, lending) that use partial notes can't adopt offchain delivery without foundational changes. Offchain delivery currently only works with direct note creation. - -2. **Standard Token contract hardcodes delivery mode.** `transfer()` uses `ONCHAIN_UNCONSTRAINED` with no way to choose offchain delivery from the outside. Every contract wanting to offer offchain delivery must add a separate method (or delivery mode should become a parameter on standard methods). - -3. **"Duplicate method, swap delivery mode" pattern.** `transfer_offchain` is identical to `transfer` except for the delivery mode constant. This suggests delivery mode could be a parameter rather than requiring method duplication. - -4. **Self-delivery is manual friction (F-324).** The sender must explicitly call `offchain_receive` for their own change note. This is boilerplate every app must handle. Automatic self-delivery would eliminate this. - -5. **Sender has no feedback channel.** With direct offchain delivery (no escrow), the sender cannot know if the recipient received or processed the offchain message. This is inherent to the "fire and forget" model but worth documenting as a tradeoff. - -6. **Recipient address required upfront.** Because notes are encrypted for a specific recipient, there's no "send to anyone with the link" pattern possible. The link only works for the intended recipient. - -7. **URL-based delivery is feasible.** Estimated ~2,100 chars for a transfer link — within browser URL limits and QR code capacity. This validates URLs as a practical delivery channel for single-note transfers. From 2b03c9607722b7b86ea0e8c8201788e3fd2d959c Mon Sep 17 00:00:00 2001 From: mverzilli Date: Mon, 13 Apr 2026 14:51:08 +0000 Subject: [PATCH 18/24] refactor fpc deployment script --- scripts/deploy-subscription-fpc.ts | 332 ++++++++++++++++++++--------- 1 file changed, 237 insertions(+), 95 deletions(-) diff --git a/scripts/deploy-subscription-fpc.ts b/scripts/deploy-subscription-fpc.ts index 94b04f4..6035f84 100644 --- a/scripts/deploy-subscription-fpc.ts +++ b/scripts/deploy-subscription-fpc.ts @@ -3,141 +3,283 @@ * * Usage: node --experimental-transform-types scripts/deploy-subscription-fpc.ts */ - import fs from 'fs'; import path from 'path'; import { SubscriptionFPC } from '@gregojuice/contracts/subscription-fpc'; +import { SubscriptionFPCContract, SubscriptionFPCContractArtifact } from '@gregojuice/contracts/artifacts/SubscriptionFPC'; import { FunctionSelector } from '@aztec/stdlib/abi'; +import type { ContractArtifact } from '@aztec/aztec.js/abi'; +import { AztecAddress } from '@aztec/stdlib/aztec-address'; +import { SponsoredFeePaymentMethod } from '@aztec/aztec.js/fee'; +import { EmbeddedWallet } from '@aztec/wallets/embedded'; import { L1FeeJuicePortalManager } from '@aztec/aztec.js/ethereum'; import { waitForL1ToL2MessageReady } from '@aztec/aztec.js/messaging'; import { createExtendedL1Client } from '@aztec/ethereum/client'; import { createLogger } from '@aztec/foundation/log'; import { foundry } from 'viem/chains'; import { Fr } from '@aztec/foundation/curves/bn254'; +import { FeeJuiceContract } from '@aztec/aztec.js/protocol'; import { ProofOfPasswordContractArtifact } from '../contracts/target/ProofOfPassword.ts'; import { AMMContractArtifact } from '../contracts/target/AMM.ts'; import { TokenContractArtifact } from '../contracts/target/Token.ts'; import { setupWallet, getOrCreateDeployer } from './utils.ts'; +import type { AztecNode } from '@aztec/aztec.js/node'; + +interface FpcSignupSpec { + artifact: ContractArtifact; + functionName: string; + contractAlias: string[]; + /** Max sponsored calls per subscribed user. Falls back to DEFAULTS.fpcSignupDefaults.maxUses. */ + maxUses?: number; + /** Max fee (in FJ wei) the FPC will cover per sponsored call. Falls back to DEFAULTS.fpcSignupDefaults.maxFee. */ + maxFee?: bigint; + /** Max concurrent subscribers for this slot. Falls back to DEFAULTS.fpcSignupDefaults.maxUsers. */ + maxUsers?: number; +} + +const DEFAULTS = { + // Path to the network config file to load/update. + // Overridable via NETWORK_CONFIG_PATH env var. + configPath: path.join(import.meta.dirname, '../src/config/networks/local.json'), + + // L1 RPC URL used for fee juice bridging. + // Defaults to local Anvil. Persisted to the network config on first run. + l1RpcUrl: 'http://localhost:8545', -// Well-known Anvil account #0 — used to sign the L1 bridge transaction on local sandbox -const ANVIL_KEY_0 = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'; + // Private key used to sign L1 transactions during FPC setup (fee juice bridging, etc.). + // Defaults to Anvil's pre-funded account #0 for local sandbox development. + // Persisted to the network config on first run so it can be overridden per-network. + l1FunderKey: '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80', + + // Default sign-up parameters applied to each FpcSignupSpec that doesn't override them. + fpcSignupDefaults: { + maxUses: 100, + maxFee: BigInt('1000000000000000000000'), // 1000 FJ + maxUsers: 100, + }, + + // Contract functions to sign up on the SubscriptionFPC. + fpcSignups: [ + { + artifact: ProofOfPasswordContractArtifact, + functionName: 'check_password_and_mint', + contractAlias: ['pop'], + }, + { + artifact: AMMContractArtifact, + functionName: 'swap_tokens_for_exact_tokens_from', + contractAlias: ['amm'], + }, + { + artifact: TokenContractArtifact, + functionName: 'transfer_in_private_deliver_offchain', + contractAlias: ['gregoCoin', 'gregoCoinPremium'], + }, + ] as FpcSignupSpec[], +}; async function main() { - const { wallet, node, paymentMethod } = await setupWallet('http://localhost:8080', 'local'); - const deployer = await getOrCreateDeployer(wallet, paymentMethod); + const configPath = process.env.NETWORK_CONFIG_PATH ?? DEFAULTS.configPath; + const config = loadConfig(configPath); - console.log('Deploying SubscriptionFPC...'); - const { deployment, secretKey } = await SubscriptionFPC.deployWithKeys(wallet, deployer); - const receipt = await deployment.send({ from: deployer, fee: { paymentMethod } }); - const fpcAddress = receipt.contract.address.toString(); - console.log('SubscriptionFPC deployed at:', fpcAddress); - console.log('Secret key:', secretKey.toString()); + const { wallet, node, paymentMethod } = await setupWallet(config.nodeUrl, 'local'); + const fpcDeployer = await getOrCreateDeployer(wallet, paymentMethod); - // Compute function selectors - const popFn = ProofOfPasswordContractArtifact.functions.find(f => f.name === 'check_password_and_mint'); - const popSelector = await FunctionSelector.fromNameAndParameters(popFn!.name, popFn!.parameters); + const { fpcAddress, secretKey } = await deployAndRegisterSubscriptionFpc(node, wallet, fpcDeployer, paymentMethod); - const ammFn = AMMContractArtifact.functions.find(f => f.name === 'swap_tokens_for_exact_tokens_from'); - const ammSelector = await FunctionSelector.fromNameAndParameters(ammFn!.name, ammFn!.parameters); + // Order matters here: + // + // 1. bridgeTokens() submits the L1 mint + bridge tx. This is only the L1 half of the flow: the L1->L2 message is now + // pending and needs the L2 sequencer to pick it up before we can claim. On local setups L2 sequencer is quiet + // when nothing else is happening, so we can't just wait. + // + // 2. executeFpcSignUps() fires a burst of L2 txs (one per sponsored function). Beyond their functional purpose, + // these txs force L2 block production, which advances the chain past the checkpoint containing our pending bridge + // message. + // + // 3. claimFeeJuiceOnL2() claims tx crediting the FPC's public fee juice balance so it can actually sponsor user + // calls. + // + // If we did them in the "obvious" order (bridge -> claim -> sign_up), the claim would hang forever waiting for an L2 + // block that never comes... so it is a bit of a hack, but it works. + const feeJuiceClaim = await bridgeTokens(node, config.l1RpcUrl, config.l1FunderKey, fpcAddress); + const signedUpFunctions = await executeFpcSignUps(fpcAddress, fpcDeployer, wallet, paymentMethod, config.contracts); + await claimFeeJuiceOnL2(node, feeJuiceClaim, wallet, fpcAddress, fpcDeployer, paymentMethod); + updateNetworkConfigFile(config, fpcAddress, secretKey, signedUpFunctions, configPath); +} - const transferOffchainFn = TokenContractArtifact.functions.find(f => f.name === 'transfer_in_private_deliver_offchain'); - const transferOffchainSelector = await FunctionSelector.fromNameAndParameters( - transferOffchainFn!.name, - transferOffchainFn!.parameters, - ); +main().catch(err => { + console.error(err); + process.exit(1); +}); - // Update local.json - const configPath = path.join(import.meta.dirname, '../src/config/networks/local.json'); +function loadConfig(configPath: string) { const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); + config.l1RpcUrl = config.l1RpcUrl ?? DEFAULTS.l1RpcUrl; + config.l1FunderKey = config.l1FunderKey ?? DEFAULTS.l1FunderKey; + return config; +} + +async function claimFeeJuiceOnL2(node: AztecNode, feeJuiceClaim, wallet: EmbeddedWallet, fpcAddress: AztecAddress, fpcDeployer: AztecAddress, paymentMethod: SponsoredFeePaymentMethod) { + // Wait for the L1->L2 bridge message and claim the FJ to credit the FPC's balance. + console.log('\nWaiting for L1->L2 message sync...'); + await waitForL1ToL2MessageReady(node, Fr.fromHexString(feeJuiceClaim.messageHash), { timeoutSeconds: 120 }); + console.log('Message ready'); + + console.log('Claiming fee juice on L2 for FPC...'); + await FeeJuiceContract.at(wallet).methods + .claim(fpcAddress, feeJuiceClaim.claimAmount, feeJuiceClaim.claimSecret, feeJuiceClaim.messageLeafIndex) + .send({ from: fpcDeployer, fee: { paymentMethod } }); + console.log('FPC funded!'); +} + +// Auxiliaries +function updateNetworkConfigFile(config: any, fpcAddress: AztecAddress, secretKey: Fr, signedUpFunctions: ResolvedSignup[], configPath: string) { config.subscriptionFPC = { - address: fpcAddress, + address: fpcAddress.toString(), secretKey: secretKey.toString(), - functions: { - [config.contracts.pop]: { - [popSelector.toString()]: 0, - }, - [config.contracts.amm]: { - [ammSelector.toString()]: 0, - }, - [config.contracts.gregoCoin]: { - [transferOffchainSelector.toString()]: 0, - }, - [config.contracts.gregoCoinPremium]: { - [transferOffchainSelector.toString()]: 0, - }, - }, + functions: buildFunctionsMap(signedUpFunctions), }; fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); console.log(`\nUpdated ${configPath} with subscriptionFPC config.`); +} - // Re-register the FPC contract with its secret key so PXE can compute tagging secrets - const { SubscriptionFPCContractArtifact: fpcArtifact } = await import('@gregojuice/contracts/artifacts/SubscriptionFPC'); - const fpcInstance = await node.getContract(receipt.contract.address); - if (!fpcInstance) throw new Error('FPC contract not found on-chain after deploy'); - await wallet.registerContract(fpcInstance, fpcArtifact, secretKey); - - // Start the L1 bridge early so the message can propagate while we do sign_up on L2. - // On local sandbox, the fee asset handler mints a fixed amount per call (1000 FJ). - // When mint=true, bridgeTokensPublic must match this exact amount. +async function bridgeTokens(node: AztecNode, l1RpcUrl: string, l1FunderKey: string, fpcAddress: AztecAddress) { + const l1Client = createExtendedL1Client([l1RpcUrl], l1FunderKey, foundry); + const portalManager = await L1FeeJuicePortalManager.new(node, l1Client, createLogger('bridge')); + const bridgeAmount: bigint = BigInt('1000000000000000000000'); // 1000 FJ - console.log(`\nBridging ${bridgeAmount} wei of fee juice to FPC...`); - const l1Client = createExtendedL1Client(['http://localhost:8545'], ANVIL_KEY_0, foundry); - const portalManager = await L1FeeJuicePortalManager.new(node, l1Client, createLogger('bridge')); - const claim = await portalManager.bridgeTokensPublic(receipt.contract.address, bridgeAmount, true); + // When mint=true, bridgeTokensPublic must match the exact bridgeAmount. + const claim = await portalManager.bridgeTokensPublic(fpcAddress, bridgeAmount, true); console.log('L1 bridge tx mined.'); + return claim; +} +/** + * Executes sign_up transactions on the SubscriptionFPC for each resolved signup. + * Must be called by the FPC admin with a working payment method. + */ +async function executeFpcSignUps( + fpcAddress: AztecAddress, + fpcDeployer: AztecAddress, + wallet: EmbeddedWallet, + paymentMethod: SponsoredFeePaymentMethod, + contracts: Record, +): Promise { // Sign up functions so users can subscribe. These L2 txs also advance the L2 chain, // which helps the sequencer include the pending L1->L2 bridge message. - const { SubscriptionFPCContract } = await import('@gregojuice/contracts/artifacts/SubscriptionFPC'); - const fpc = SubscriptionFPCContract.at(receipt.contract.address, wallet); - - const maxUses = 100; - const maxFee = BigInt('1000000000000000000000'); // 1000 FJ - const maxUsers = 100; - - const popAddress = config.contracts.pop; - console.log(`\nSigning up PoP selector ${popSelector} at index 0...`); - await fpc.methods - .sign_up(popAddress, popSelector, 0, maxUses, maxFee, maxUsers) - .send({ from: deployer, fee: { paymentMethod } }); - console.log('PoP sign_up done!'); - - const ammAddress = config.contracts.amm; - console.log(`Signing up AMM selector ${ammSelector} at index 0...`); - await fpc.methods - .sign_up(ammAddress, ammSelector, 0, maxUses, maxFee, maxUsers) - .send({ from: deployer, fee: { paymentMethod } }); - console.log('AMM sign_up done!'); - - // Sign up transfer_in_private_deliver_offchain on both token contracts - for (const tokenKey of ['gregoCoin', 'gregoCoinPremium'] as const) { - const tokenAddress = config.contracts[tokenKey]; - console.log(`Signing up ${tokenKey}.transfer_in_private_deliver_offchain at index 0...`); + const functionsToSignupToFpc = await resolveFpcSignups( + DEFAULTS.fpcSignups, + contracts, + DEFAULTS.fpcSignupDefaults, + ); + + const fpc = SubscriptionFPCContract.at(fpcAddress, wallet); + + for (const { addressKey, contractAddress, functionName, selector, maxUses, maxFee, maxUsers } of functionsToSignupToFpc) { + console.log(`\nSigning up ${addressKey}.${functionName} at index 0...`); await fpc.methods - .sign_up(tokenAddress, transferOffchainSelector, 0, maxUses, maxFee, maxUsers) - .send({ from: deployer, fee: { paymentMethod } }); - console.log(`${tokenKey} sign_up done!`); + .sign_up(contractAddress, selector, 0, maxUses, maxFee, maxUsers) + .send({ from: fpcDeployer, fee: { paymentMethod } }); + console.log(`${addressKey}.${functionName} sign_up done!`); } - // Wait for the L1->L2 bridge message and claim the FJ to credit the FPC's balance. - console.log('\nWaiting for L1->L2 message sync...'); - const messageHash = Fr.fromHexString(claim.messageHash); - await waitForL1ToL2MessageReady(node, messageHash, { timeoutSeconds: 120 }); - console.log('Message ready'); + return functionsToSignupToFpc; +} - const { FeeJuiceContract } = await import('@aztec/aztec.js/protocol'); - const feeJuice = FeeJuiceContract.at(wallet); - console.log('Claiming fee juice on L2 for FPC...'); - await feeJuice.methods - .claim(receipt.contract.address, claim.claimAmount, claim.claimSecret, claim.messageLeafIndex) - .send({ from: deployer, fee: { paymentMethod } }); - console.log('FPC funded!'); +/** + * Deploys a new SubscriptionFPC with fresh keys. The secret key is generated during + * deployment and must be persisted (clients need it to decrypt the FPC's slot notes). + */ +async function deploySubscriptionFpc( + wallet: EmbeddedWallet, + deployer: AztecAddress, + paymentMethod: SponsoredFeePaymentMethod, +): Promise<{ address: AztecAddress; secretKey: Fr }> { + console.log('Deploying SubscriptionFPC...'); + const { deployment, secretKey } = await SubscriptionFPC.deployWithKeys(wallet, deployer); + const receipt = await deployment.send({ from: deployer, fee: { paymentMethod } }); + const address = receipt.contract.address; + console.log('SubscriptionFPC deployed at:', address.toString()); + console.log('Secret key:', secretKey.toString()); + return { address, secretKey }; } -main().catch(err => { - console.error(err); - process.exit(1); -}); +/** A sign-up spec with its selector computed and sponsorship params resolved. */ +interface ResolvedSignup { + addressKey: string; + contractAddress: AztecAddress; + functionName: string; + selector: FunctionSelector; + maxUses: number; + maxFee: bigint; + maxUsers: number; +} + +/** + * Resolves a list of FpcSignupSpecs into concrete (contractAddress, selector) tuples, + * merging per-spec overrides with the defaults. + */ +async function resolveFpcSignups( + specs: FpcSignupSpec[], + contracts: Record, + defaults: { maxUses: number; maxFee: bigint; maxUsers: number }, +): Promise { + return Promise.all( + specs.flatMap(spec => { + const fn = spec.artifact.functions.find(f => f.name === spec.functionName); + if (!fn) { + throw new Error(`Function ${spec.functionName} not found in artifact ${spec.artifact.name}`); + } + const maxUses = spec.maxUses ?? defaults.maxUses; + const maxFee = spec.maxFee ?? defaults.maxFee; + const maxUsers = spec.maxUsers ?? defaults.maxUsers; + return spec.contractAlias.map(async addressKey => { + const rawAddress = contracts[addressKey]; + if (!rawAddress) { + throw new Error(`Address key "${addressKey}" not found in config.contracts`); + } + const contractAddress = AztecAddress.fromString(rawAddress); + const selector = await FunctionSelector.fromNameAndParameters(fn.name, fn.parameters); + return { addressKey, contractAddress, functionName: spec.functionName, selector, maxUses, maxFee, maxUsers }; + }); + }), + ); +} + +/** + * Builds the subscriptionFPC.functions map from resolved signups: + * `{ contractAddress: { selectorHex: configIndex } }`. + */ +function buildFunctionsMap(resolved: ResolvedSignup[]): Record> { + const map: Record> = {}; + for (const { contractAddress, selector } of resolved) { + const key = contractAddress.toString(); + map[key] = map[key] ?? {}; + map[key][selector.toString()] = 0; + } + return map; +} + +async function deployAndRegisterSubscriptionFpc(node: AztecNode, wallet: EmbeddedWallet, deployer: AztecAddress, paymentMethod: SponsoredFeePaymentMethod) { + const { address: fpcAddress, secretKey } = await deploySubscriptionFpc(wallet, deployer, paymentMethod); + + // `deployWithKeys` deploys with derived public keys (so the PXE knows the contract's + // address + public keys), but never communicates the secret key to the PXE, it only + // returns it to us. `sign_up` emits SlotNotes at `self.storage.slots.at(self.address)` + // and calls `set_sender_for_tags(self.address)`, which requires the PXE to know the + // secret key corresponding to the FPC's address so it can compute tagging secrets. + // + // We add the secret key to the already-registered instance via the third arg of + // `registerContract`. Without this, `sign_up` later fails with "No public key registered for + // address 0x...". TODO: push this step into gregojuice's `deployWithKeys` upstream so + // callers don't need the follow-up. + const fpcInstance = await node.getContract(fpcAddress); + if (!fpcInstance) throw new Error('FPC contract not found on-chain after deploy'); + await wallet.registerContract(fpcInstance, SubscriptionFPCContractArtifact, secretKey); + + return { fpcAddress, secretKey }; +} From dc36f5caa5e4805b1aaebf7e32d3b4338f8b3568 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Mon, 13 Apr 2026 15:07:45 +0000 Subject: [PATCH 19/24] remove outdated script --- scripts/signup-fpc.ts | 71 ------------------------------------------- 1 file changed, 71 deletions(-) delete mode 100644 scripts/signup-fpc.ts diff --git a/scripts/signup-fpc.ts b/scripts/signup-fpc.ts deleted file mode 100644 index ae73504..0000000 --- a/scripts/signup-fpc.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Calls sign_up on the SubscriptionFPC for each configured function (PoP + AMM). - * Uses the same deployer wallet as deploy-subscription-fpc.ts. - * - * Usage: node --experimental-transform-types scripts/signup-fpc.ts - */ - -import fs from 'fs'; -import path from 'path'; -import { AztecAddress } from '@aztec/stdlib/aztec-address'; -import { FunctionSelector } from '@aztec/stdlib/abi'; -import { SubscriptionFPCContract } from '@gregojuice/contracts/artifacts/SubscriptionFPC'; -import { ProofOfPasswordContractArtifact } from '../contracts/target/ProofOfPassword.ts'; -import { AMMContractArtifact } from '../contracts/target/AMM.ts'; -import { setupWallet, getOrCreateDeployer } from './utils.ts'; - -async function main() { - const configPath = path.join(import.meta.dirname, '../src/config/networks/local.json'); - const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); - - const { wallet, node, paymentMethod } = await setupWallet('http://localhost:8080', 'local'); - const adminAddress = await getOrCreateDeployer(wallet, paymentMethod); - - const fpcAddress = AztecAddress.fromString(config.subscriptionFPC.address); - - // Register the FPC contract in this PXE so it can interact with it - const { SubscriptionFPCContractArtifact } = await import('@gregojuice/contracts/artifacts/SubscriptionFPC'); - const fpcInstance = await node.getContract(fpcAddress); - if (!fpcInstance) throw new Error('FPC contract not found on-chain'); - await wallet.registerContract(fpcInstance, SubscriptionFPCContractArtifact); - - const fpc = SubscriptionFPCContract.at(fpcAddress, wallet); - console.log('Admin:', adminAddress.toString()); - console.log('FPC:', fpcAddress.toString()); - - // sign_up params: generous for local dev - const maxUses = 100; - const maxFee = BigInt('1000000000000000000000'); // 1000 FJ in wei - const maxUsers = 100; - - // 1. Sign up PoP.check_password_and_mint - const popFn = ProofOfPasswordContractArtifact.functions.find(f => f.name === 'check_password_and_mint'); - const popSelector = await FunctionSelector.fromNameAndParameters(popFn!.name, popFn!.parameters); - const popAddress = AztecAddress.fromString(config.contracts.pop); - const popConfigIndex = config.subscriptionFPC.functions[config.contracts.pop][popSelector.toString()]; - - console.log(`\nSigning up PoP (${popAddress}) selector ${popSelector} at index ${popConfigIndex}...`); - await fpc.methods - .sign_up(popAddress, popSelector, popConfigIndex, maxUses, maxFee, maxUsers) - .send({ from: adminAddress, fee: { paymentMethod } }); - console.log('PoP sign_up done!'); - - // 2. Sign up AMM.swap_tokens_for_exact_tokens_from - const ammFn = AMMContractArtifact.functions.find(f => f.name === 'swap_tokens_for_exact_tokens_from'); - const ammSelector = await FunctionSelector.fromNameAndParameters(ammFn!.name, ammFn!.parameters); - const ammAddress = AztecAddress.fromString(config.contracts.amm); - const ammConfigIndex = config.subscriptionFPC.functions[config.contracts.amm][ammSelector.toString()]; - - console.log(`\nSigning up AMM (${ammAddress}) selector ${ammSelector} at index ${ammConfigIndex}...`); - await fpc.methods - .sign_up(ammAddress, ammSelector, ammConfigIndex, maxUses, maxFee, maxUsers) - .send({ from: adminAddress, fee: { paymentMethod } }); - console.log('AMM sign_up done!'); - - console.log('\nAll functions signed up successfully!'); -} - -main().catch(err => { - console.error(err); - process.exit(1); -}); From a379b3e9b5042d319abe719d632ab45bbca44739 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Mon, 13 Apr 2026 15:20:22 +0000 Subject: [PATCH 20/24] minor --- src/components/claim/ClaimPage.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/claim/ClaimPage.tsx b/src/components/claim/ClaimPage.tsx index 5cf2504..a788422 100644 --- a/src/components/claim/ClaimPage.tsx +++ b/src/components/claim/ClaimPage.tsx @@ -1,5 +1,7 @@ import { Box, Typography, Button, Alert, CircularProgress, Container, Chip } from '@mui/material'; import { useEffect, useState, useCallback } from 'react'; +import { Fr } from '@aztec/aztec.js/fields'; +import { AztecAddress } from '@aztec/aztec.js/addresses'; import { extractClaimPayload, type TransferLink } from '../../services/offchainLinkService'; import { ClaimProgress } from './ClaimProgress'; import { ClaimSuccess } from './ClaimSuccess'; @@ -55,9 +57,6 @@ export function ClaimPage() { } catch { /* new wallet may have no balance */ } // Reconstruct Fr values and call offchain_receive - const { Fr } = await import('@aztec/aztec.js/fields'); - const { AztecAddress } = await import('@aztec/aztec.js/addresses'); - const tokenKey = data.token === 'gc' ? 'gregoCoin' as const : 'gregoCoinPremium' as const; await claimOffchainTransfer(tokenKey, { From ed2c4a33f49a6eee16fd2fec68e468211637c2e3 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Mon, 13 Apr 2026 15:43:31 +0000 Subject: [PATCH 21/24] remove redundant type declaration --- src/contexts/contracts/ContractsContext.tsx | 2 +- src/services/contractService.ts | 12 +----------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/src/contexts/contracts/ContractsContext.tsx b/src/contexts/contracts/ContractsContext.tsx index 8e53b97..ed64b9b 100644 --- a/src/contexts/contracts/ContractsContext.tsx +++ b/src/contexts/contracts/ContractsContext.tsx @@ -8,7 +8,7 @@ import type { AztecAddress } from '@aztec/aztec.js/addresses'; import type { Fr } from '@aztec/foundation/curves/bn254'; import type { TxReceipt } from '@aztec/stdlib/tx'; import type { AMMContract } from '../../../contracts/target/AMM'; -import type { OffchainMessage } from '../../services/contractService'; +import type { OffchainMessage } from '@aztec/aztec.js/contracts'; import { useWallet } from '../wallet'; import { useNetwork } from '../network'; import * as contractService from '../../services/contractService'; diff --git a/src/services/contractService.ts b/src/services/contractService.ts index 428326a..449ec20 100644 --- a/src/services/contractService.ts +++ b/src/services/contractService.ts @@ -9,7 +9,7 @@ import { AztecAddress } from '@aztec/aztec.js/addresses'; import { AztecAddress as AztecAddressClass } from '@aztec/aztec.js/addresses'; import { Fr } from '@aztec/aztec.js/fields'; import { FunctionSelector } from '@aztec/aztec.js/abi'; -import { BatchCall, getContractInstanceFromInstantiationParams } from '@aztec/aztec.js/contracts'; +import { BatchCall, getContractInstanceFromInstantiationParams, type OffchainMessage } from '@aztec/aztec.js/contracts'; import { poseidon2Hash } from '@aztec/foundation/crypto/poseidon'; import type { TxReceipt } from '@aztec/stdlib/tx'; import type { TokenContract } from '../../contracts/target/Token'; @@ -541,16 +541,6 @@ export async function executeDrip( return receipt; } -/** - * Offchain message returned by transfer_in_private_deliver_offchain - */ -export interface OffchainMessage { - recipient: AztecAddress; - payload: Fr[]; - contractAddress: AztecAddress; - anchorBlockTimestamp: bigint; -} - /** * Execute an offchain token transfer. * Sends tokens privately with offchain note delivery, self-delivers the sender's From 467ccfe2c47cf1eb8487f36cd6c5a162f03d5980 Mon Sep 17 00:00:00 2001 From: mverzilli Date: Mon, 13 Apr 2026 16:51:14 +0000 Subject: [PATCH 22/24] ux improvements --- src/App.tsx | 186 ++++++++++++-------------- src/components/claim/ClaimPage.tsx | 28 ++-- src/components/claim/ClaimSuccess.tsx | 6 +- src/components/send/SendContainer.tsx | 59 +++++++- src/components/send/SendForm.tsx | 18 ++- 5 files changed, 179 insertions(+), 118 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index e72aece..e96b170 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { ThemeProvider, CssBaseline, Container, Box, Typography, Tabs, Tab, Snackbar } from '@mui/material'; import { theme } from './theme'; import { GregoSwapLogo } from './components/GregoSwapLogo'; @@ -18,9 +18,18 @@ import type { AztecAddress } from '@aztec/aztec.js/addresses'; export function App() { const [activeTab, setActiveTab] = useState(0); const [addressCopied, setAddressCopied] = useState(false); + const [onClaimRoute, setOnClaimRoute] = useState(isClaimRoute); const { disconnectWallet, setCurrentAddress, currentAddress, error: walletError, isLoading: walletLoading } = useWallet(); const { isOnboardingModalOpen, startOnboarding, resetOnboarding, status: onboardingStatus } = useOnboarding(); + // Re-evaluate the claim route whenever the URL hash changes so that pasting a claim + // link into an already-loaded tab (or clicking an in-app link) routes correctly. + useEffect(() => { + const handler = () => setOnClaimRoute(isClaimRoute()); + window.addEventListener('hashchange', handler); + return () => window.removeEventListener('hashchange', handler); + }, []); + const isOnboarded = onboardingStatus === 'completed'; const handleWalletClick = async () => { @@ -38,40 +47,6 @@ export function App() { resetOnboarding(); }; - if (isClaimRoute()) { - return ( - - - - - - - ); - } - return ( @@ -117,78 +92,89 @@ export function App() { /> - {/* Header */} - - - - - - Swap GregoCoin for GregoCoinPremium - - - - {/* Tab Bar */} - setActiveTab(value)} - centered - sx={{ - mb: 3, - '& .MuiTab-root': { color: 'text.secondary', fontWeight: 600 }, - '& .Mui-selected': { color: 'primary.main' }, - '& .MuiTabs-indicator': { backgroundColor: 'primary.main' }, - }} - > - - - - - {/* Tab Content */} - {activeTab === 0 && } - {activeTab === 1 && } - - {/* Wallet Error Display */} - {walletError && ( - - - - Wallet Connection Error - - - {walletError} + {onClaimRoute ? ( + { + setActiveTab(1); // land on the Send tab after claiming + window.location.hash = ''; + }} + /> + ) : ( + <> + {/* Header */} + + + + + + Swap GregoCoin for GregoCoinPremium - - )} - {/* Loading Display */} - {walletLoading && !walletError && ( - - setActiveTab(value)} + centered sx={{ - p: 3, - backgroundColor: 'rgba(212, 255, 40, 0.05)', - border: '1px solid rgba(212, 255, 40, 0.2)', - borderRadius: 1, - textAlign: 'center', + mb: 3, + '& .MuiTab-root': { color: 'text.secondary', fontWeight: 600 }, + '& .Mui-selected': { color: 'primary.main' }, + '& .MuiTabs-indicator': { backgroundColor: 'primary.main' }, }} > - - Connecting to network... - - - - )} + + + - {/* Footer Info */} - + {/* Tab Content */} + {activeTab === 0 && } + {activeTab === 1 && } + + {/* Wallet Error Display */} + {walletError && ( + + + + Wallet Connection Error + + + {walletError} + + + + )} + + {/* Loading Display */} + {walletLoading && !walletError && ( + + + + Connecting to network... + + + + )} + + {/* Footer Info */} + + + )} diff --git a/src/components/claim/ClaimPage.tsx b/src/components/claim/ClaimPage.tsx index a788422..0afb2d8 100644 --- a/src/components/claim/ClaimPage.tsx +++ b/src/components/claim/ClaimPage.tsx @@ -1,4 +1,4 @@ -import { Box, Typography, Button, Alert, CircularProgress, Container, Chip } from '@mui/material'; +import { Box, Typography, Button, Alert, CircularProgress, Chip } from '@mui/material'; import { useEffect, useState, useCallback } from 'react'; import { Fr } from '@aztec/aztec.js/fields'; import { AztecAddress } from '@aztec/aztec.js/addresses'; @@ -17,7 +17,11 @@ type ClaimState = | { phase: 'claimed'; data: TransferLink; verified: boolean } | { phase: 'error'; message: string }; -export function ClaimPage() { +interface ClaimPageProps { + onClaimComplete: () => void; +} + +export function ClaimPage({ onClaimComplete }: ClaimPageProps) { const [state, setState] = useState({ phase: 'decoding' }); const { claimOffchainTransfer, registerBaseContracts, fetchBalances, isLoadingContracts } = useContracts(); const { wallet, currentAddress } = useWallet(); @@ -82,17 +86,19 @@ export function ClaimPage() { } }, [state, wallet, currentAddress, isLoadingContracts, registerBaseContracts, fetchBalances, claimOffchainTransfer]); - const handleGoToSwap = () => { - window.location.hash = ''; - window.location.reload(); - }; + // After a successful claim, return to the main app and land on the Send tab. + // We just clear the hash and call the parent's callback — no reload, so the + // user's session (wallet, onboarding, contracts) is preserved. + const handleGoToSend = onClaimComplete; const tokenName = (t: string) => (t === 'gc' ? 'GregoCoin' : 'GregoCoinPremium'); return ( - - - + + + + + {state.phase === 'decoding' && ( @@ -113,10 +119,10 @@ export function ClaimPage() { {state.phase === 'claiming' && } {state.phase === 'verifying' && } {state.phase === 'claimed' && ( - + )} {state.phase === 'error' && {state.message}} - + ); } diff --git a/src/components/claim/ClaimSuccess.tsx b/src/components/claim/ClaimSuccess.tsx index b66efbf..6296dbe 100644 --- a/src/components/claim/ClaimSuccess.tsx +++ b/src/components/claim/ClaimSuccess.tsx @@ -5,10 +5,10 @@ interface ClaimSuccessProps { amount: string; tokenName: string; verified: boolean; - onGoToSwap: () => void; + onGoToSend: () => void; } -export function ClaimSuccess({ amount, tokenName, verified, onGoToSwap }: ClaimSuccessProps) { +export function ClaimSuccess({ amount, tokenName, verified, onGoToSend }: ClaimSuccessProps) { return ( @@ -17,7 +17,7 @@ export function ClaimSuccess({ amount, tokenName, verified, onGoToSwap }: ClaimS {amount} {tokenName} - + ); } diff --git a/src/components/send/SendContainer.tsx b/src/components/send/SendContainer.tsx index 19cec0c..81f6fe8 100644 --- a/src/components/send/SendContainer.tsx +++ b/src/components/send/SendContainer.tsx @@ -1,4 +1,4 @@ -import { Box, Alert } from '@mui/material'; +import { Box, Alert, Dialog, DialogTitle, DialogContent, CircularProgress, Typography } from '@mui/material'; import { useSend } from '../../contexts/send'; import { useWallet } from '../../contexts/wallet'; import { useContracts } from '../../contexts/contracts'; @@ -6,13 +6,19 @@ import { SendForm } from './SendForm'; import { SendProgress } from './SendProgress'; import { LinkDisplay } from './LinkDisplay'; import { SentHistory } from './SentHistory'; +import { DripPasswordInput } from '../onboarding/DripPasswordInput'; +import { parseDripError } from '../../services/contractService'; import { useEffect, useState } from 'react'; +type FaucetPhase = 'idle' | 'registering' | 'awaiting_password' | 'dripping'; + export function SendContainer() { const { phase, error, generatedLink, token, amount, recipientAddress, dismissError, reset } = useSend(); const { currentAddress } = useWallet(); - const { fetchBalances } = useContracts(); + const { fetchBalances, registerDripContracts, drip } = useContracts(); const [balances, setBalances] = useState<{ gc: bigint | null; gcp: bigint | null }>({ gc: null, gcp: null }); + const [faucetPhase, setFaucetPhase] = useState('idle'); + const [faucetError, setFaucetError] = useState(null); useEffect(() => { if (currentAddress) { @@ -26,18 +32,65 @@ export function SendContainer() { } }, [phase, currentAddress, fetchBalances]); + const handleOpenFaucet = async () => { + setFaucetError(null); + setFaucetPhase('registering'); + try { + await registerDripContracts(); + setFaucetPhase('awaiting_password'); + } catch (err) { + setFaucetError(err instanceof Error ? err.message : 'Failed to register drip contracts'); + setFaucetPhase('idle'); + } + }; + + const handleDripSubmit = async (password: string) => { + if (!currentAddress) return; + setFaucetPhase('dripping'); + try { + await drip(password, currentAddress); + const [gc, gcp] = await fetchBalances(); + setBalances({ gc, gcp }); + setFaucetPhase('idle'); + } catch (err) { + setFaucetError(parseDripError(err)); + setFaucetPhase('awaiting_password'); + } + }; + + const closeDialog = () => { + if (faucetPhase === 'dripping') return; // don't allow close while in-flight + setFaucetPhase('idle'); + setFaucetError(null); + }; + return ( {phase === 'link_ready' && generatedLink ? ( ) : ( <> - + )} {error && {error}} + {faucetError && setFaucetError(null)} sx={{ mt: 2 }}>{faucetError}} {currentAddress && } + + + Get tokens from faucet + + {faucetPhase === 'dripping' ? ( + + + Claiming tokens... + + ) : ( + + )} + + ); } diff --git a/src/components/send/SendForm.tsx b/src/components/send/SendForm.tsx index e091fc6..af8c29d 100644 --- a/src/components/send/SendForm.tsx +++ b/src/components/send/SendForm.tsx @@ -1,14 +1,18 @@ import { Box, TextField, Typography, ToggleButton, ToggleButtonGroup, Button } from '@mui/material'; +import WaterDropIcon from '@mui/icons-material/WaterDrop'; import { useSend } from '../../contexts/send'; interface SendFormProps { balance: { gc: bigint | null; gcp: bigint | null }; + onRequestFaucet: () => void; + faucetBusy: boolean; } -export function SendForm({ balance }: SendFormProps) { +export function SendForm({ balance, onRequestFaucet, faucetBusy }: SendFormProps) { const { token, recipientAddress, amount, phase, setToken, setRecipientAddress, setAmount, canSend, executeSend } = useSend(); const isSending = phase === 'sending' || phase === 'generating_link'; const currentBalance = token === 'gc' ? balance.gc : balance.gcp; + const selectedTokenIsEmpty = currentBalance === 0n; return ( @@ -26,6 +30,18 @@ export function SendForm({ balance }: SendFormProps) { setAmount(e.target.value)} fullWidth disabled={isSending} size="small" slotProps={{ input: { endAdornment: currentBalance !== null ? Balance: {currentBalance.toString()} : null } }} /> + {selectedTokenIsEmpty && ( + + )} From 7eabe4f65992e8b62d1ab8ed975e0f473ae9dddb Mon Sep 17 00:00:00 2001 From: mverzilli Date: Mon, 13 Apr 2026 17:06:06 +0000 Subject: [PATCH 23/24] readme and yarn task --- README.md | 37 +++++++++++++++++++++++++++++++++++-- package.json | 1 + 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 47149b1..93c4b59 100644 --- a/README.md +++ b/README.md @@ -97,8 +97,41 @@ This will: - Deploy the ProofOfPassword contract - Generate a `deployed-addresses.json` file with contract addresses -#### 4. Start the Development Server +#### 4. Deploy the Subscription FPC + +GregoSwap uses a [SubscriptionFPC](https://github.com/Thunkar/gregojuice) (Fee Payment +Contract) to sponsor user transactions: the drip, the swap, and the offchain send all +run for free from the user's perspective, with the FPC paying gas in Fee Juice. + +If you are testing on a local network, you will need to bootstrap FPC infrastructure. + +After the base contracts are in place, you can deploy and configure the FPC with: + +```bash +yarn deploy:fpc:local +``` + +This single command does everything needed to bring the FPC online against the local +sandbox: + +- Deploys a fresh `SubscriptionFPC` with generated keys +- Bridges fee juice from L1 (Anvil) to the FPC's L2 address so it can actually pay gas +- Calls `sign_up` on the FPC for each sponsored function declared in + `scripts/deploy-subscription-fpc.ts` (currently: `PoP.check_password_and_mint`, + `AMM.swap_tokens_for_exact_tokens_from`, and + `Token.transfer_in_private_deliver_offchain` on both token contracts) +- Claims the L1→L2 message on behalf of the FPC so its balance is usable +- Writes the FPC address, secret key, and function-selector map into + `src/config/networks/local.json` under `subscriptionFPC` + +The script is idempotent over the underlying config: re-running it deploys a new FPC +and overwrites the `subscriptionFPC` block. You can use a different config file via +`NETWORK_CONFIG_PATH`. + +Note this is not needed to test on Testnet's or Mainnet's, since there the SubscriptionFPC infrastructure is already set up. + +#### 5. Start the Development Server ```bash -yarn serve +yarn dev ``` diff --git a/package.json b/package.json index 48fa2cc..4127a07 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "deploy:devnet": "node --experimental-transform-types scripts/deploy.ts --network devnet", "deploy:nextnet": "node --experimental-transform-types scripts/deploy.ts --network nextnet", "deploy:testnet": "node --experimental-transform-types scripts/deploy.ts --network testnet", + "deploy:fpc:local": "node --experimental-transform-types scripts/deploy-subscription-fpc.ts", "mint:local": "node --experimental-transform-types scripts/mint.ts --network local", "mint:testnet": "node --experimental-transform-types scripts/mint.ts --network testnet", "formatting": "run -T prettier --check ./src && run -T eslint ./src", From 72fe7aeb4f24b06776d02629dc7d4a8475ac2be1 Mon Sep 17 00:00:00 2001 From: thunkar Date: Fri, 17 Apr 2026 14:24:36 +0200 Subject: [PATCH 24/24] fixes --- src/contexts/contracts/ContractsContext.tsx | 86 ++++++++++++--------- src/services/contractService.ts | 9 +-- 2 files changed, 52 insertions(+), 43 deletions(-) diff --git a/src/contexts/contracts/ContractsContext.tsx b/src/contexts/contracts/ContractsContext.tsx index a40ad67..badce03 100644 --- a/src/contexts/contracts/ContractsContext.tsx +++ b/src/contexts/contracts/ContractsContext.tsx @@ -14,6 +14,7 @@ import { useWallet } from '../wallet'; import { useNetwork } from '../network'; import * as contractService from '../../services/contractService'; import { useContractsReducer } from './reducer'; +import { stat } from 'fs'; interface ContractsContextType { isLoadingContracts: boolean; @@ -31,8 +32,15 @@ interface ContractsContextType { fetchBalances: () => Promise<[bigint, bigint]>; simulateOnboardingQueries: () => Promise<[number, bigint, bigint]>; drip: (password: string, recipient: AztecAddress) => Promise; - sendOffchain: (tokenKey: 'gregoCoin' | 'gregoCoinPremium', recipient: AztecAddress, amount: bigint) => Promise<{ receipt: TxReceipt; offchainMessages: OffchainMessage[] }>; - claimOffchainTransfer: (tokenKey: 'gregoCoin' | 'gregoCoinPremium', message: { ciphertext: Fr[]; recipient: AztecAddress; tx_hash: Fr; anchor_block_timestamp: bigint }) => Promise; + sendOffchain: ( + tokenKey: 'gregoCoin' | 'gregoCoinPremium', + recipient: AztecAddress, + amount: bigint, + ) => Promise<{ receipt: TxReceipt; offchainMessages: OffchainMessage[] }>; + claimOffchainTransfer: ( + tokenKey: 'gregoCoin' | 'gregoCoinPremium', + message: { ciphertext: Fr[]; recipient: AztecAddress; tx_hash: Fr; anchor_block_timestamp: bigint }, + ) => Promise; } const ContractsContext = createContext(undefined); @@ -243,42 +251,48 @@ export function ContractsProvider({ children }: ContractsProviderProps) { ); // Execute offchain transfer (send with link) - const sendOffchain = useCallback(async ( - tokenKey: 'gregoCoin' | 'gregoCoinPremium', - recipient: AztecAddress, - amount: bigint, - ) => { - if (!wallet || !currentAddress || !state.contracts.gregoCoin || !state.contracts.gregoCoinPremium || !state.contracts.amm) { - throw new Error('Contracts not initialized'); - } - return contractService.executeTransferOffchain( - wallet, - activeNetwork, - { - gregoCoin: state.contracts.gregoCoin, - gregoCoinPremium: state.contracts.gregoCoinPremium, - amm: state.contracts.amm, - }, - tokenKey, - currentAddress, - recipient, - amount, - ); - }, [wallet, activeNetwork, currentAddress, state.contracts]); + const sendOffchain = useCallback( + async (tokenKey: 'gregoCoin' | 'gregoCoinPremium', recipient: AztecAddress, amount: bigint) => { + if ( + !wallet || + !currentAddress || + !state.contracts.gregoCoin || + !state.contracts.gregoCoinPremium || + !state.contracts.amm + ) { + throw new Error('Contracts not initialized'); + } + return contractService.executeTransferOffchain( + activeNetwork, + { + gregoCoin: state.contracts.gregoCoin, + gregoCoinPremium: state.contracts.gregoCoinPremium, + amm: state.contracts.amm, + fpc: state.contracts.fpc, + }, + tokenKey, + currentAddress, + recipient, + amount, + ); + }, + [wallet, activeNetwork, currentAddress, state.contracts], + ); // Claim an offchain transfer via offchain_receive - const claimOffchainTransfer = useCallback(async ( - tokenKey: 'gregoCoin' | 'gregoCoinPremium', - message: { ciphertext: Fr[]; recipient: AztecAddress; tx_hash: Fr; anchor_block_timestamp: bigint }, - ) => { - if (!wallet || !currentAddress || !state.contracts.gregoCoin || !state.contracts.gregoCoinPremium) { - throw new Error('Contracts not initialized'); - } - const token = tokenKey === 'gregoCoin' ? state.contracts.gregoCoin : state.contracts.gregoCoinPremium; - await token.methods - .offchain_receive([message]) - .simulate({ from: currentAddress }); - }, [wallet, currentAddress, state.contracts]); + const claimOffchainTransfer = useCallback( + async ( + tokenKey: 'gregoCoin' | 'gregoCoinPremium', + message: { ciphertext: Fr[]; recipient: AztecAddress; tx_hash: Fr; anchor_block_timestamp: bigint }, + ) => { + if (!wallet || !currentAddress || !state.contracts.gregoCoin || !state.contracts.gregoCoinPremium) { + throw new Error('Contracts not initialized'); + } + const token = tokenKey === 'gregoCoin' ? state.contracts.gregoCoin : state.contracts.gregoCoinPremium; + await token.methods.offchain_receive([message]).simulate({ from: currentAddress }); + }, + [wallet, currentAddress, state.contracts], + ); // Initialize contracts for embedded wallet useEffect(() => { diff --git a/src/services/contractService.ts b/src/services/contractService.ts index fcd9b25..f844756 100644 --- a/src/services/contractService.ts +++ b/src/services/contractService.ts @@ -545,7 +545,6 @@ export async function executeDrip( * change note, and returns the recipient's offchain messages for link encoding. */ export async function executeTransferOffchain( - wallet: Wallet, network: NetworkConfig, contracts: SwapContracts, tokenKey: 'gregoCoin' | 'gregoCoinPremium', @@ -558,6 +557,8 @@ export async function executeTransferOffchain( throw new Error('No subscriptionFPC configured for this network'); } + const fpc = contracts.fpc; + const token = contracts[tokenKey]; const authwitNonce = Fr.random(); @@ -572,12 +573,6 @@ export async function executeTransferOffchain( ); } - const fpcAddress = AztecAddressClass.fromString(subFPC.address); - const { SubscriptionFPCContract } = await import('@gregojuice/contracts/artifacts/SubscriptionFPC'); - const { SubscriptionFPC } = await import('@gregojuice/contracts/subscription-fpc'); - const rawFPC = SubscriptionFPCContract.at(fpcAddress, wallet); - const fpc = new SubscriptionFPC(rawFPC); - const subscribed = hasSubscription(subFPC.address, configIndex, fromAddress.toString()); let txResult: { receipt: TxReceipt; offchainMessages: OffchainMessage[] };