Repository navigation
feat(js): add guarded SOL transfer helper - #96
Conversation
ee983f6 to
e127f83
Compare
joncinque
left a comment
There was a problem hiding this comment.
Just from a quick pass, since the checks are all completely independent of the instruction creation, how about just exposing the checks as a new function?
When we discussed a new instruction creator, that would be for something like requiring the destination to sign. We shouldn't need it here.
@lorisleiva can you take a look?
Thx for looking. Fair point--dropped the instruction wrapper and left the assertion. |
trevor-cortex
left a comment
There was a problem hiding this comment.
Summary
Adds a hand-written assertValidTransferSolDestination(rpc, destination, config?) helper to the JS client. It fetches the destination account and throws a plain Error (with a structured cause) if the account is owned by an unexpected program, doesn't exist (unless allowUnfundedRecipient), or is off-curve (only when allowOffCurve: false). Exported from index.ts alongside the generated code, with LiteSVM-backed tests covering each branch and a mock test checking that guard options aren't leaked into getAccountInfo.
The implementation is small and correct for what it sets out to do, and the test coverage is good. My comments are mostly about API shape, since this is the first non-generated export in this package and will set the precedent for the token / token-2022 follow-ups mentioned in the description.
Things to consider before merging
1. Scope: first hand-written code in a generated client. src/index.ts was previously just export * from './generated'. That's fine in principle (other solana-program clients do it), but it's worth @joncinque / @lorisleiva explicitly deciding this is the home for it rather than, say, a Kit-level or docs-level helper, given the stated plan to replicate it across token clients.
2. Throwing vs. classifying. The PR's own example is an exchange prompting the user for confirmation. With an assert-only API, that flow has to try/catch, then narrow e.cause by hand. A non-throwing classifier (e.g. getTransferSolDestinationStatus(...) returning { status: 'valid' } | { status: 'unexpected-owner', owner } | ...) plus a thin assert* wrapper over it follows Kit's isX / assertIsX pattern and serves both the "prompt the user" and the "hard fail" use cases. Not a blocker, but cheaper to decide now than after a token-client copy exists.
3. Error typing. A plain Error with an untyped cause means consumers can't instanceof or narrow without a cast. See inline comment on the cause type.
4. Defaults. Unfunded destinations are a hard failure by default while off-curve ones pass by default. Both are "confirm with the user" cases in the address-verification guide, so the asymmetry is worth a sentence of justification in the docs or a rethink. See inline.
For subsequent reviewers
- No
.changeset/in this repo, so nothing to check there. This is a new public export, so it'll want a minor bump on the next JS release. isOffCurveAddressfrom@solana/kitis synchronous, so the ordering (owner → unfunded → off-curve) is deterministic and matches the docblock.- The
does not forward guard options to getAccountInfotest relies onfetchEncodedAccountspreading{ ...rpcConfig, encoding: 'base64' }— that's Kit's current behaviour, but it's a test of Kit internals as much as of this helper.
Validates a transferSol destination before building the instruction, rejecting accounts not owned by the expected program and unfunded recipients unless explicitly allowed. Off-curve destinations are allowed by default and can be rejected with allowOffCurve: false. Aligns with the payments address verification guide.
…ation - Throw a typed InvalidTransferSolDestinationError instead of Error + cause - Rename TransferSolGuardConfig to AssertValidTransferSolDestinationConfig - Type rpc as Rpc<GetAccountInfoApi> - Accept any InstructionAccountInput as the destination, matching transferSol - Document why off-curve destinations are allowed by default - Drop product names from the off-curve error message
3ddcca1 to
9bdee15
Compare
|
trevor-cortex
left a comment
There was a problem hiding this comment.
Re-review
All the points from the first pass have been addressed:
InvalidTransferSolDestinationErrorreplacesError+ untypedcause, with typeddestination/reason/ownerfields andoverride readonly name. Tests now usetoBeInstanceOfand match on the fields directly.- Config type renamed to
AssertValidTransferSolDestinationConfig. rpcis typed asRpc<GetAccountInfoApi>.- Destination accepts
InstructionAccountInputand is unwrapped withgetAddressFromResolvedInstructionAccount('destination', input), which is exactly how the hand-written helpers in program-metadata and token-2022 do it, and matches what the generatedtransferSolbuilder accepts. New tests coverHasAddress, signer and PDA-tuple inputs. - Both defaults now have a one-line rationale in the docblock (unfunded → likely typo; off-curve → multisigs are common), which is what I was after. The asymmetry is a defensible product call once it's written down.
- Product names dropped from the off-curve message.
The classifier-vs-assert question (#2 in my first review) wasn't taken up, which is fine as it was explicitly non-blocking; the typed error class makes try/catch + instanceof + reason a perfectly usable path for the "prompt the user" flow.
Minor, non-blocking
- The docblock calls out that an off-curve account owned by another program (e.g. an ATA) reports
'unexpected-owner'rather than'off-curve'because the owner check runs first. That ordering is the one non-obvious behaviour in the function and isn't covered by a test. A single case (allowOffCurve: false+ Token-owned PDA →reason: 'unexpected-owner') would pin it down.
For maintainers
- No
.changeset/in this repo. This adds new public exports (assertValidTransferSolDestination,InvalidTransferSolDestinationError,InvalidTransferSolDestinationReason,AssertValidTransferSolDestinationConfig), so the next JS release should be a minor bump. - The
@solana/kit/program-client-coresubpath import is already used by the generated code, so no new peer-dep requirement.
LGTM.
Summary
assertValidTransferSolDestinationandto validate a SOL transfer destination before buildinggetTransferSolGuardedInstructiontransferSol, preventing sends to unrecoverable addresses. This will help add clarity for exchanges/organizations handling user transfers.allowOffCurve: false.A destination is rejected unless it's an existing account owned by the expected program (default: System Program). Rejections throw a plain
Errorwhosecauseis{ destination, reason, owner? }:reasonunexpected-ownerprogramOwnerunfunded-recipientallowUnfundedRecipient: trueoff-curveallowOffCurve: falseallowOffCurve: true)Example Use
Simulated example of how an exchange might use this to prompt user for confirmation before sending a transfer.