feat(arns): Model-A custodial credit-buy for Arweave/ETH identities (multi-wallet) - #957
feat(arns): Model-A custodial credit-buy for Arweave/ETH identities (multi-wallet)#957vilenarios wants to merge 7 commits into
Conversation
- Re-enable the Credits checkout tab, wired to the bundler payment-service REST (POST /v1/arns/purchase/:intent/:name) via the turbo-sdk ArNS-purchase methods; credits are debited server-side and the bundler fronts ARIO + SOL. - Cover buy-name / extend-lease / increase-undername / upgrade-name. - Model B: the Solana user owns the ANT (spawned client-side; processId passed to the bundler). @ar.io/sdk fundFrom:'turbo' is a no-op alias, so settlement is rerouted to the bundler REST. - Add nonce poll + resilience (arnsPurchaseResume) and a custody strategy seam (custodyStrategy.ts) for the future Model A path. - Make upload/gateway/payment URLs env-configurable. - Add an esbuild (dev) + rollup (build) codec shim rewriting the renamed getDataEnumCodec -> getDiscriminatedUnionCodec import dragged in by @solana/spl-token-metadata, so the browser build resolves. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The ArNS-purchase methods are now published on the @Alpha dist-tag (ardriveapp/turbo-sdk#435 merged). Replaces the local-tarball / placeholder pin so CI installs a real, resolvable version and the build is reproducible. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
…aid BUY The Model B credits BUY flow spawns a user-owned ANT client-side (real SOL) before a server-fronted, credit-funded buy. Several P0 gaps let a user lose SOL, hit dead-end errors, or misread what they were paying: - SOL pre-flight gate now covers the credits path, not just crypto. Paying with credits still needs ~0.02 SOL for the ANT spawn, so a credits-rich / SOL-empty wallet is blocked + the "Pay now" button relabels to "Insufficient SOL" BEFORE clicking. Extracted the predicate into a pure, unit-tested checkInsufficientSolForGas() and added copy on the Credits tab. - Runtime 402 (InsufficientCreditsError) now opens the TurboTopUpModal instead of a dead-end error toast. - Orphan-ANT / SOL-bleed fix: persist the spawned ANT processId into the resume store BEFORE the buy; a retry REUSES it instead of spawning (and paying for) another. A non-402 failure after spawn surfaces an honest "your ANT was created, retrying reuses it, credits weren't charged" message. - Removed the stale "temporarily disabled" tooltip on the now-live Credits tab. - Added USD + ARIO real-world anchors next to the "Total due: X Credits" figure. - Invalidate turbo-credit-balance after a purchase so the debit shows at once. - PrimaryNameModal: documented that set-primary-name is ARIO-only (the credit path type excludes Primary-Name-Request); it never presented a credits option, so no user is misled — added a guard comment against a future fundFrom:'turbo'. Tests: new unit suites for the SOL gate, resume store (processId persistence), and the dispatch reuse-on-retry / honest-message / 402-passthrough behavior. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CFu1gpBLo6pe5kBFNw5NF9
Restore the multi-wallet infrastructure removed in the Solana-only refactor and wire the credit-paid CUSTODIAL ArNS buy (Model A) for non-Solana identities: the user pays Turbo Credits, the bundler spawns + owns the ANT (no client-side SOL/ANT-spawn). Solana (Model B, user-owned ANT) is untouched. Restored (from 3f43b85): - WALLET_TYPES: re-add ARWEAVE/WANDER/ETHEREUM alongside SOLANA (types.ts) - WanderWalletConnector (Arweave) + EthWalletConnector (ETH); turboSigner is an arbundles ArconnectSigner / InjectedEthereumSigner usable by turbo-sdk - WagmiProvider + RainbowKitProvider mounted AROUND the Solana provider stack in main.tsx; un-stub the wagmi hooks in Checkout / CryptoConfirmation / PaymentOptionsForm - ConnectWalletModal: surface Wander (Arweave) + Ethereum options next to Solana - WalletState: wire Arweave/ETH connectors with turboSigner populated; SKIP the Solana signer ARIO.init for non-Solana identities Model A wiring: - TurboArNSClient.buildAuthenticatedArNSClient is now identity-agnostic: branches on tokenType — arweave/ethereum use TurboFactory.authenticated({ token, signer }) - custodyStrategy A-custodial.isStub -> false - dispatchArNSPurchaseWithCredits accepts arweave/ethereum via Model A: no ANT spawn, buy with processId OMITTED (custodial), pass tokenType + turboSigner - Custodial UX: CustodialNameNotice on the completion surface + a Claim/transfer-out placeholder (POST /v1/arns/transfer seam stubbed) Tests: identity-agnostic client + Model-A (arweave) path (processId omitted, signer required). Validated on devnet against the bundler at :4001: an Arweave-signed custodial buy debited credits (1,524,656,593,974 winc) and the bundler spawned + owns the ANT (user_ant maps ANT->buyer; name resolves on-chain). ETH path compiles + is wired but is NOT live-validated (its bundler request-auth fix lands separately). Deferred: keyless onboarding, sponsored spawn, swap widget, the credit-authed claim/exit flow. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CFu1gpBLo6pe5kBFNw5NF9
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Permaweb Preview DeployedYour preview is available at: https://arns-react-pr-957_arns.ar.io
|
Implements the self-custody escape hatch so a Model-A custodial ArNS name
(bought with Turbo Credits by an Arweave/Ethereum identity, ANT owned by the
bundler's server signer) can be moved out to a Solana wallet the user controls.
Client:
- TurboArNSClient.transferCustodialArNSName({ antId, target }) now calls the
authenticated turbo-sdk transferArNSAnt({ antId, target }) instead of throwing
a "not available yet" stub. The credit-identity signer (solana/arweave/eth)
authenticates; the SDK builds the ACTION-BOUND message (arns\ntransfer\n
{antId}\n{target} + single-use nonce) — never hand-rolled. Typed errors:
InvalidTransferTargetError, CustodialANTNotFoundError (404, non-leaky),
CustodyTransferUnauthorizedError (401). Extracted a shared
buildAuthenticatedTurboClient so the purchase + custody paths share identity
handling.
UX (CustodialNameNotice):
- "Claim / Transfer to a wallet" opens a form: Solana-pubkey target input with
live base58/32-byte validation, an explicit irreversible-action warning, and
a required confirmation checkbox. Transfer is gated until target is valid AND
the user has confirmed. On success the notice flips to a self-custodied state
showing the new owner. Wired through TransactionComplete (passes name).
Security / threat model:
- Wrong-target: target validated as a real Solana pubkey before any signature;
permanent-loss warning + deliberate confirm required.
- Replay: signed message is action-bound + single-use nonce (SDK), so a captured
signature can't move a different ANT or to a different recipient; request is
not retried on 5xx.
- Not-owner: bundler authorizes against custody and returns a deliberately
conflated 404 (not-found == not-yours); surfaced without leaking other names.
Tests: unit coverage for param passthrough, target validation (valid/malformed/
empty), 404/401 mapping, and the confirm gate. Validated live on devnet: a
custodial arweave buy's ANT moved on-chain from the Turbo server signer to a
fresh Solana pubkey (owner before/after verified via SolanaANTReadable), and a
second custody op then 404s (user_ant mapping cleared).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFu1gpBLo6pe5kBFNw5NF9
… credit-paid record management Make a non-Solana (Arweave / Ethereum) identity able to actually USE a custodial (Model A) ArNS name end-to-end: fund credits, and manage the name's records with those credits (the user does not own the Turbo-held ANT, so the wallet-signed manage path physically can't work for them). Solana Model B is untouched. Part 1 — identity-agnostic credit top-up - TurboTopUpModal: seed the crypto tab's default token from the CONNECTED wallet.tokenType instead of a hardcoded Solana/Arweave assumption; drop the dead localStorage-driven branch. Fiat top-up was already identity-agnostic (credits `walletAddress` under `wallet.tokenType`) — verified live: minting to a checksummed ETH and an Arweave address resolves the balance the buy flow spends (the bundler is case-sensitive on ETH, and both top-up + balance use the checksummed wagmi address, so they stay consistent). - CryptoConfirmation: replace the "ArConnect wallet is not supported in Solana mode" hard-throw (the last Solana-only dead code) with a real AR/ARIO top-up via the wallet's arbundles turbo signer — mirrors the wired ETH-ARIO branch, so credits land on the connected Arweave address. Part 2 — credit-paid custodial record management - TurboArNSClient: add setCustodialArNSRecord / removeCustodialArNSRecord (identity-agnostic authed client -> SDK setArNSRecord/removeArNSRecord, which builds the action-bound signed message); map bundler 404->CustodialANTNotFound, 401->CustodyTransferUnauthorized (non-leaky). - New dispatchCustodialANTRecordInteraction action routes SET_TARGET_ID / SET_TTL_SECONDS -> apex `@` record and SET_RECORD/EDIT_RECORD/REMOVE_RECORD -> undername records through credits, driving transaction state like the wallet-signed dispatchANTInteraction. - DomainSettings (Target ID + TTL) and UndernamesTable route through the credit path for custodial names and show a "paid with credits" indication; Model B (self-owned) keeps the wallet-signed path unchanged. Detection is structural: a non-Solana identity can never own/control a Solana ANT, so an ANT-backed name it isn't owner/controller of is credit-managed — the bundler is the authority (authorizes per custody mapping, non-leaky 404 surfaced cleanly). The custodian's Solana signer address is not exposed by the bundler, so we do not compare owner addresses. Tests: client set/remove record (mapping + 404/401), identity-agnostic top-up across arweave/ethereum/solana, and the custodial dispatch routing. Live devnet validation (bundler :4001): an Arweave-signed custodial buy debited 2,654,741,255,337 winc and Turbo spawned + owns the ANT; set `@`, set + remove an undername each returned an on-chain Solana tx signature (record ops themselves are free). On-chain via @ar.io/sdk (devnet DEVNET_PROGRAM_IDS): the `@` record transactionId matches what was set and the removed undername is absent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CFu1gpBLo6pe5kBFNw5NF9
… (Model A polish) Protocol reality on Solana ario-ant: transfer wipes controllers and spawn always makes the signer the sole owner+controller, so "user owns the ANT AND Turbo edits records gaslessly" can't be set up. The two coherent shapes are (A) Turbo-custodied (gasless buy + gasless credit-paid record management + exit via /transfer) and (B) user self-owns (record edits need the user's own Solana signature + SOL). Custody is purely identity-driven today (resolveCustodyStrategy: solana -> self-owned, arweave/eth -> custodial) with NO custody-choice UI, so there is nothing to "default" to the gasless path — the gasless-managed path is already the implicit outcome for credit-paying (non-Solana) identities. Instead, make the self-owned path honest: - DomainSettings: show a concise, non-alarming note on the self-managed surface (self-owned Solana / Model B name) that record edits are wallet-signed and cost a little SOL — so an owner with an empty SOL balance isn't surprised on their first edit. - CustodialNameNotice: at the claim/exit decision point (where a Model A user chooses to self-own), add that after claiming they manage the name themselves — edits need their Solana signature + SOL (no longer gasless credit-paid). Polish: - CryptoConfirmation: drop two leftover debug console.log lines in the fund-retry path (keep error logging). - WalletState: drop two tagged [WalletState] console.debug/console.info dev logs (keep the console.error). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CFu1gpBLo6pe5kBFNw5NF9
What this unlocks
Restores multi-wallet and delivers the complete Model-A (Turbo-custodied) ArNS user journey for Arweave / Ethereum identities: connect a non-Solana wallet, fund Turbo Credits to your native address, buy a name (Turbo custodies the ANT — no client SOL, no client ANT-spawn), manage its records with credits (target
@+ undernames, gasless), and claim/exit to a Solana wallet anytime. Solana (Model B, user-owned ANT) is untouched.Protocol reality that shapes this: on Solana
ario-ant,transferwipes controllers andspawnalways makes the signer the sole owner+controller — so "user owns the ANT AND Turbo edits records gaslessly" cannot be set up. The two coherent custody shapes are therefore (A) Turbo-custodied (gasless buy + gasless credit-paid record management + exit via/transfer) and (B) user self-owns (record edits need the user's own Solana signature + SOL). Custody is identity-driven (arweave/eth → A, solana → B); there is no custody-choice toggle, so the gasless-managed path is already the implicit outcome for credit-paying identities.Restored (multi-wallet infra)
WALLET_TYPES:ARWEAVE/WANDER/ETHEREUMalongsideSOLANA.WanderWalletConnector(Arweave) +EthWalletConnector(wagmi/viem); each exposes aturboSigner(arbundlesArconnectSigner/InjectedEthereumSigner) that plugs intoTurboFactory.authenticated({ token, signer }).main.tsx:WagmiProvider+RainbowKitProvideraround the Solana stack (both coexist); wagmi hooks un-stubbed.ConnectWalletModalsurfaces Wander + Ethereum.WalletStatewires the connectors and skips the SolanaARIO.initfor non-Solana identities.Credit top-up (identity-agnostic)
TurboTopUpModal: seeds the crypto tab's default token from the connectedwallet.tokenType(no hardcoded Solana/Arweave assumption). Fiat top-up already creditswalletAddressunderwallet.tokenType, so it funds the connected identity's native address.CryptoConfirmation: the Arweave crypto path now performs a real AR/ARIO top-up via the wallet's turbo signer (mirrors the wired ETH-ARIO branch), replacing a dead "not supported in Solana mode" throw.Buy + record management (credits)
TurboArNSClient.buildAuthenticatedArNSClient/buildAuthenticatedTurboClientare identity-agnostic (branch ontokenType).dispatchArNSPurchaseWithCredits: Model A buys withprocessIdomitted (custodial provisioning), no ANT spawn; Model B unchanged.TurboArNSClient.setCustodialArNSRecord/removeCustodialArNSRecord→ SDKsetArNSRecord/removeArNSRecord(POST /v1/arns/manage/:antId/{set,remove}-record, action-bound signed message); 404→CustodialANTNotFoundError, 401→CustodyTransferUnauthorizedError(non-leaky).dispatchCustodialANTRecordInteractionroutesSET_TARGET_ID/SET_TTL_SECONDS→ apex@andSET_RECORD/EDIT_RECORD/REMOVE_RECORD→ undernames through credits.DomainSettings(Target ID + TTL) andUndernamesTableroute custodial names through the credit path with a "paid with credits" indication; Model B keeps the wallet-signed path. Detection is structural (a non-Solana identity can never own/control a Solana ANT); the bundler is the authority (authorizes per custody mapping, non-leaky 404 surfaced cleanly).Claim / self-custody exit
CustodialNameNotice: credit-authed exitPOST /v1/arns/transfer/:antId?target=<solanaPubkey>— validated Solana target, irreversible-action confirmation, action-bound single-use signature, non-leaky error mapping.Honest self-ownership (SOL) warnings
DomainSettings: a concise, non-alarming note on the self-managed (Solana-owned) surface that record edits are wallet-signed and cost a little SOL.CustodialNameNotice: at the claim/exit decision point, a note that after claiming, edits need the user's Solana signature + SOL (no longer gasless).Live devnet validation (bundler
:4001)FKCDWrXWb4MprQoHTSiSZP3yC1xVBFqEjtJqgBL7Nk9hwith an on-chainmessageId.set @,set+removean undername each returned an on-chain Solana tx signature (record ops themselves are free). Verified on-chain via@ar.io/sdk(devnetDEVNET_PROGRAM_IDS): the@recordtransactionIdmatches what was set, and the removed undername is absent.Gates
tsc --noEmit: clean except the pre-existingSOLANA_MIGRATION_LINKbaseline error.yarn lint:check(biome, 342 files): clean.yarn build(Vite): succeeds.yarn test: 61 pass, 0 fail; 6 unrelated suites fail to load on the pre-existing@ar.io/solana-contracts/arnsmodule-resolution error (untouched suites).Deferred
🤖 Generated with Claude Code
https://claude.ai/code/session_01CFu1gpBLo6pe5kBFNw5NF9