Skip to content

feat(arns): Model-A custodial credit-buy for Arweave/ETH identities (multi-wallet) - #957

Draft
vilenarios wants to merge 7 commits into
developfrom
feat/arns-model-a-multiwallet
Draft

feat(arns): Model-A custodial credit-buy for Arweave/ETH identities (multi-wallet)#957
vilenarios wants to merge 7 commits into
developfrom
feat/arns-model-a-multiwallet

Conversation

@vilenarios

@vilenarios vilenarios commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Stacked on #956 (feat/turbo-credits-arns). Review/merge that first; this branch adds the non-Solana identity path on top of the credits infra.

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, transfer wipes controllers and spawn always 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/ETHEREUM alongside SOLANA.
  • WanderWalletConnector (Arweave) + EthWalletConnector (wagmi/viem); each exposes a turboSigner (arbundles ArconnectSigner / InjectedEthereumSigner) that plugs into TurboFactory.authenticated({ token, signer }).
  • main.tsx: WagmiProvider + RainbowKitProvider around the Solana stack (both coexist); wagmi hooks un-stubbed. ConnectWalletModal surfaces Wander + Ethereum. WalletState wires the connectors and skips the Solana ARIO.init for non-Solana identities.

Credit top-up (identity-agnostic)

  • TurboTopUpModal: seeds the crypto tab's default token from the connected wallet.tokenType (no hardcoded Solana/Arweave assumption). Fiat top-up already credits walletAddress under wallet.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/buildAuthenticatedTurboClient are identity-agnostic (branch on tokenType).
  • dispatchArNSPurchaseWithCredits: Model A buys with processId omitted (custodial provisioning), no ANT spawn; Model B unchanged.
  • New: TurboArNSClient.setCustodialArNSRecord / removeCustodialArNSRecord → SDK setArNSRecord/removeArNSRecord (POST /v1/arns/manage/:antId/{set,remove}-record, action-bound signed message); 404→CustodialANTNotFoundError, 401→CustodyTransferUnauthorizedError (non-leaky).
  • New: dispatchCustodialANTRecordInteraction routes SET_TARGET_ID/SET_TTL_SECONDS → apex @ and SET_RECORD/EDIT_RECORD/REMOVE_RECORD → undernames through credits.
  • DomainSettings (Target ID + TTL) and UndernamesTable route 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 exit POST /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)

  • Buy (Arweave-signed, custodial): debited 2,654,741,255,337 winc; Turbo spawned + owns ANT FKCDWrXWb4MprQoHTSiSZP3yC1xVBFqEjtJqgBL7Nk9h with an on-chain messageId.
  • Record management (credits): set @, set + remove an undername each returned an on-chain Solana tx signature (record ops themselves are free). Verified on-chain via @ar.io/sdk (devnet DEVNET_PROGRAM_IDS): the @ record transactionId matches what was set, and the removed undername is absent.
  • Top-up: minting to a checksummed ETH and an Arweave address resolves the balance the buy flow spends (bundler is case-sensitive on ETH; the app uses the checksummed wagmi address consistently for both top-up and balance).

Gates

  • tsc --noEmit: clean except the pre-existing SOLANA_MIGRATION_LINK baseline 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/arns module-resolution error (untouched suites).

Deferred

  • Ethereum crypto top-up (mainnet BrowserProvider path) is wired but not live-validated (needs real funds). Keyless onboarding, sponsored spawn, swap widget.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CFu1gpBLo6pe5kBFNw5NF9

vilenarios and others added 4 commits July 15, 2026 01:26
- 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
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d89c528e-b8e0-4a4b-8bb8-57d6addaccda

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/arns-model-a-multiwallet

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown

Permaweb Preview Deployed

Your preview is available at: https://arns-react-pr-957_arns.ar.io

Property Value
Transaction ID 32m9wn1I6Gb_86sv-3-1MJkmXEI9FuTR-zUKmq-YMHp
ArNS Name arns
Undername arns-react-pr-957

This preview will be available permanently on Arweave via the AR.IO Network.

vilenarios and others added 3 commits July 15, 2026 16:34
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant