Skip to content

fix(sdk-coin-near): validate receiving address in NEAR tx builders#9292

Draft
bitgo-ai-agent-dev[bot] wants to merge 1 commit into
masterfrom
coins-1221-near-receiver-address-validation
Draft

fix(sdk-coin-near): validate receiving address in NEAR tx builders#9292
bitgo-ai-agent-dev[bot] wants to merge 1 commit into
masterfrom
coins-1221-near-receiver-address-validation

Conversation

@bitgo-ai-agent-dev

@bitgo-ai-agent-dev bitgo-ai-agent-dev Bot commented Jul 20, 2026

Copy link
Copy Markdown

Summary

NEAR transaction builders were not validating the recipient (receiverId/ftReceiverId/beneficiaryId) address before including it in a transaction. As a result, invalid or non-existent addresses could pass through and be broadcast on-chain, where they'd fail with AccountDoesNotExist, burning gas with no way for the user to have known in advance (COINS-1221).

This PR closes that gap in two parts: fixing the address-format validation that was silently being ignored, and adding an on-chain existence check for the recipient before broadcast.

Context

  • receiverId(), ftReceiverId(), and beneficiaryId() all called utils.isValidAddress(accountId) but discarded its boolean return value, so malformed addresses (empty, spaces, special characters, over 64 chars) were silently accepted. sender() in the same file already validated correctly and threw — the receiving-address setters had the same gap and were simply missed.

  • NEAR has two account formats, and only one of them needs an existence check. Named accounts (e.g. alice.near, pool.f863973.m0) are human-readable and must be explicitly created on-chain via a CreateAccount action before anything can be sent to them. Implicit accounts (64-char lowercase hex, a raw ed25519 public key) don't need to be pre-created — a transfer to a fresh, never-used implicit account is itself how it gets created on-chain.

    The reported issue (COINS-1221) was exactly this gap for named accounts: a user submitted a transfer to a syntactically valid but never-created named receiver_id. isValidAddress() is a purely offline regex/format check — it validates that a string looks like a named account, but has no way to know whether that named account was ever actually created. Because NEAR (unlike EVM, where any address is inherently valid and needs no "creation") distinguishes between well-formed and existing accounts, format validation alone cannot catch this — it requires an on-chain RPC lookup, which is what the second half of this PR adds.

Changes

Address format validation (transactionBuilder.ts, fungibleTokenTransferBuilder.ts, storageDepositTransferBuilder.ts)

  • Each setter now throws AddressValidationError when utils.isValidAddress() returns false, matching the existing sender() pattern.

On-chain existence check (near.ts)

  • Added doesAccountExist(), reusing the existing getDataFromNode() / view_account RPC pattern already used by getAccountBalance().
  • Wired into verifyTransaction() for all recipient-bearing transaction types except enabletoken (its recipient is the wallet's own storage/token contract, not a user-supplied destination).
  • Implicit (64-char hex) accounts are exempt from the check — verified against NEAR testnet RPC that an unfunded implicit account also reports UNKNOWN_ACCOUNT, even though sending to it is a valid way to create it on-chain. Only named accounts are existence-checked.

Decision log

  1. Error type for the format fixAddressValidationError over BuildTransactionError: it's the purpose-built type for this case and extends BuildTransactionError, so existing callers catching BuildTransactionError are unaffected.
  2. Where the existence check lives — builder setters are synchronous and can't make RPC calls; verifyTransaction() is already async and already has the parsed recipient available, so the check lives there instead.
  3. Which transaction types get checked — initially scoped to native transfers only, generalized to all recipient-bearing types except enabletoken: a wrong validator/pool address in a staking transaction fails on-chain the same way a bad transfer recipient does.
  4. Implicit account exemption — confirmed via live RPC query that a fresh implicit account also returns UNKNOWN_ACCOUNT, so exempting it prevents false positives on legitimate first-time transfers to valid implicit accounts.

Test plan

  • receiverId(''), spaces, special chars, and over-64-char addresses throw AddressValidationError in TransactionBuilder, TransferBuilder, FungibleTokenTransferBuilder, and StorageDepositTransferBuilder
  • verifyTransaction() succeeds when the named recipient account exists on-chain (mocked RPC)
  • verifyTransaction() throws Recipient account '...' does not exist on NEAR when the RPC reports UNKNOWN_ACCOUNT
  • verifyTransaction() propagates other RPC errors from the existence check
  • Implicit (hex) recipient accounts skip the RPC call entirely
  • Staking transaction recipients are also existence-checked
  • enabletoken recipients skip the RPC call entirely
  • All 184 unit tests pass (178 existing + 6 new)

Ticket: COINS-1221

@linear-code

linear-code Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

COINS-1221

@bitgo-ai-agent-dev

Copy link
Copy Markdown
Author

@claude please review this PR

@bitgo-ai-agent-dev
bitgo-ai-agent-dev Bot force-pushed the coins-1221-near-receiver-address-validation branch from 69dd36f to 626b648 Compare July 20, 2026 10:39
…ddresses

- Fix receiverId(), ftReceiverId(), and beneficiaryId() in the NEAR
  transaction builders, which called utils.isValidAddress() but
  discarded the return value, silently accepting empty, malformed,
  or over-64-char receiving addresses. Now throw AddressValidationError,
  matching the existing pattern in sender().
- Add an on-chain recipient existence check in Near.verifyTransaction():
  queries the NEAR RPC (view_account) to confirm the recipient account
  exists before the transaction is signed/broadcast, for all
  recipient-bearing transaction types except enabletoken (whose
  recipient is the wallet's own storage/token contract, not a
  user-supplied destination). Implicit (64-char hex) accounts are
  exempt, since an unfunded implicit account also reports
  UNKNOWN_ACCOUNT even though sending to it is how such accounts are
  created on-chain.

Without either fix, a malformed or never-created receiver_id would
pass validation and be broadcast, only to fail on-chain with
AccountDoesNotExist and burn gas.

Ticket: COINS-1221
@nayandas190
nayandas190 force-pushed the coins-1221-near-receiver-address-validation branch from 02715a7 to 00b7175 Compare July 21, 2026 12:52
@nayandas190
nayandas190 marked this pull request as ready for review July 21, 2026 13:07
@nayandas190
nayandas190 requested a review from a team as a code owner July 21, 2026 13:07

@abhishekagrawal080 abhishekagrawal080 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me

@nayandas190
nayandas190 marked this pull request as draft July 21, 2026 13:59
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.

2 participants