Skip to content

Add ClientWithTransactionSigning interface - #1899

Draft
mcintyre94 wants to merge 1 commit into
tx-executor-context-onlyfrom
client-sign-transaction
Draft

Add ClientWithTransactionSigning interface#1899
mcintyre94 wants to merge 1 commit into
tx-executor-context-onlyfrom
client-sign-transaction

Conversation

@mcintyre94

@mcintyre94 mcintyre94 commented Aug 7, 2026

Copy link
Copy Markdown
Member

Summary of Changes

This PR adds the ClientWithTransactionSigning interface, which adds signTransaction[s] functions to the client.

These take the same inputs as sendTransaction. The implementation should finalise and sign the transaction, for example adding a lifetime that may not have been part of the planner.

As the transaction does not need to be sent, it is allowed to be partially signed. In particular, the fee payer does not necessarily need to have signed it, which is useful for eg. relayers. We return the TransactionPlanResultWithOptionalSignature types defined in #1893, to reflect that the fee payer may not have provided a signature, and therefore the transaction may not yet have one.

@changeset-bot

changeset-bot Bot commented Aug 7, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e9ff80a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 48 packages
Name Type
@solana/plugin-interfaces Minor
@solana/kit Minor
@solana/program-client-core Minor
@solana/react Minor
@solana/accounts Minor
@solana/addresses Minor
@solana/assertions Minor
@solana/codecs-core Minor
@solana/codecs-data-structures Minor
@solana/codecs-numbers Minor
@solana/codecs-strings Minor
@solana/codecs Minor
@solana/compat Minor
@solana/errors Minor
@solana/fast-stable-stringify Minor
@solana/fixed-points Minor
@solana/functional Minor
@solana/instruction-plans Minor
@solana/instructions Minor
@solana/keys Minor
@solana/nominal-types Minor
@solana/offchain-messages Minor
@solana/options Minor
@solana/plugin-core Minor
@solana/programs Minor
@solana/promises Minor
@solana/rpc-api Minor
@solana/rpc-graphql Minor
@solana/rpc-parsed-types Minor
@solana/rpc-spec-types Minor
@solana/rpc-spec Minor
@solana/rpc-subscriptions-api Minor
@solana/rpc-subscriptions-channel-websocket Minor
@solana/rpc-subscriptions-spec Minor
@solana/rpc-subscriptions Minor
@solana/rpc-transformers Minor
@solana/rpc-transport-http Minor
@solana/rpc-types Minor
@solana/rpc Minor
@solana/signers Minor
@solana/subscribable Minor
@solana/sysvars Minor
@solana/transaction-confirmation Minor
@solana/transaction-introspection Minor
@solana/transaction-messages Minor
@solana/transactions Minor
@solana/wallet-account-signer Minor
@solana/webcrypto-ed25519-polyfill Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

mcintyre94 commented Aug 7, 2026

Copy link
Copy Markdown
Member Author

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@bundlemon

bundlemon Bot commented Aug 7, 2026

Copy link
Copy Markdown

BundleMon

Files updated (4)
Status Path Size Limits
@solana/kit production bundle
kit/dist/index.production.min.js
55.97KB (-81B -0.14%) -
instruction-plans/dist/index.browser.mjs
6.94KB (-117B -1.62%) -
instruction-plans/dist/index.native.mjs
6.94KB (-117B -1.62%) -
instruction-plans/dist/index.node.mjs
6.94KB (-118B -1.63%) -
Unchanged files (146)
Status Path Size Limits
errors/dist/index.node.mjs
21.81KB -
errors/dist/index.browser.mjs
21.79KB -
errors/dist/index.native.mjs
21.79KB -
rpc-graphql/dist/index.browser.mjs
18.82KB -
rpc-graphql/dist/index.native.mjs
18.82KB -
rpc-graphql/dist/index.node.mjs
18.82KB -
wallet-account-signer/dist/index.node.mjs
18.5KB -
wallet-account-signer/dist/index.browser.mjs
18.48KB -
wallet-account-signer/dist/index.native.mjs
18.48KB -
transaction-messages/dist/index.browser.mjs
11.34KB -
transaction-messages/dist/index.native.mjs
11.34KB -
transaction-messages/dist/index.node.mjs
11.34KB -
react/dist/index.browser.mjs
5.32KB -
react/dist/index.native.mjs
5.32KB -
react/dist/index.node.mjs
5.32KB -
codecs-data-structures/dist/index.browser.mjs
5.29KB -
codecs-data-structures/dist/index.native.mjs
5.29KB -
codecs-data-structures/dist/index.node.mjs
5.29KB -
offchain-messages/dist/index.browser.mjs
5.25KB -
offchain-messages/dist/index.native.mjs
5.24KB -
offchain-messages/dist/index.node.mjs
5.24KB -
fixed-points/dist/index.browser.mjs
5.08KB -
fixed-points/dist/index.native.mjs
5.07KB -
fixed-points/dist/index.node.mjs
5.07KB -
kit/dist/index.browser.mjs
4.86KB -
kit/dist/index.native.mjs
4.85KB -
kit/dist/index.node.mjs
4.85KB -
transactions/dist/index.browser.mjs
4.07KB -
transactions/dist/index.native.mjs
4.07KB -
transactions/dist/index.node.mjs
4.07KB -
codecs-core/dist/index.browser.mjs
3.62KB -
codecs-core/dist/index.native.mjs
3.62KB -
codecs-core/dist/index.node.mjs
3.62KB -
webcrypto-ed25519-polyfill/dist/index.node.mj
s
3.61KB -
webcrypto-ed25519-polyfill/dist/index.browser
.mjs
3.59KB -
webcrypto-ed25519-polyfill/dist/index.native.
mjs
3.57KB -
rpc-subscriptions/dist/index.browser.mjs
3.37KB -
rpc-subscriptions/dist/index.node.mjs
3.34KB -
rpc-subscriptions/dist/index.native.mjs
3.31KB -
signers/dist/index.browser.mjs
3.26KB -
signers/dist/index.native.mjs
3.26KB -
signers/dist/index.node.mjs
3.26KB -
rpc-transformers/dist/index.browser.mjs
3.18KB -
rpc-transformers/dist/index.native.mjs
3.18KB -
rpc-transformers/dist/index.node.mjs
3.18KB -
subscribable/dist/index.node.mjs
3.13KB -
keys/dist/index.node.mjs
3.06KB -
subscribable/dist/index.native.mjs
3.06KB -
subscribable/dist/index.browser.mjs
3.05KB -
addresses/dist/index.browser.mjs
2.93KB -
addresses/dist/index.native.mjs
2.92KB -
addresses/dist/index.node.mjs
2.92KB -
keys/dist/index.browser.mjs
2.85KB -
keys/dist/index.native.mjs
2.85KB -
transaction-introspection/dist/index.browser.
mjs
2.73KB -
transaction-introspection/dist/index.native.m
js
2.73KB -
transaction-introspection/dist/index.node.mjs
2.73KB -
codecs-strings/dist/index.browser.mjs
2.55KB -
codecs-strings/dist/index.node.mjs
2.51KB -
codecs-strings/dist/index.native.mjs
2.47KB -
transaction-confirmation/dist/index.node.mjs
2.42KB -
transaction-confirmation/dist/index.native.mj
s
2.37KB -
sysvars/dist/index.browser.mjs
2.37KB -
sysvars/dist/index.native.mjs
2.37KB -
transaction-confirmation/dist/index.browser.m
js
2.37KB -
sysvars/dist/index.node.mjs
2.37KB -
rpc-subscriptions-spec/dist/index.node.mjs
2.26KB -
rpc-subscriptions-spec/dist/index.native.mjs
2.21KB -
rpc-subscriptions-spec/dist/index.browser.mjs
2.21KB -
rpc/dist/index.node.mjs
1.95KB -
codecs-numbers/dist/index.browser.mjs
1.95KB -
codecs-numbers/dist/index.native.mjs
1.95KB -
codecs-numbers/dist/index.node.mjs
1.94KB -
rpc-types/dist/index.browser.mjs
1.9KB -
rpc-types/dist/index.native.mjs
1.9KB -
rpc-types/dist/index.node.mjs
1.9KB -
rpc-transport-http/dist/index.browser.mjs
1.89KB -
rpc-transport-http/dist/index.native.mjs
1.89KB -
rpc/dist/index.native.mjs
1.81KB -
rpc/dist/index.browser.mjs
1.8KB -
rpc-transport-http/dist/index.node.mjs
1.71KB -
rpc-spec-types/dist/index.browser.mjs
1.54KB -
rpc-spec-types/dist/index.native.mjs
1.54KB -
rpc-spec-types/dist/index.node.mjs
1.54KB -
rpc-subscriptions-channel-websocket/dist/inde
x.node.mjs
1.33KB -
rpc-subscriptions-channel-websocket/dist/inde
x.native.mjs
1.27KB -
rpc-subscriptions-channel-websocket/dist/inde
x.browser.mjs
1.26KB -
program-client-core/dist/index.browser.mjs
1.21KB -
program-client-core/dist/index.native.mjs
1.21KB -
program-client-core/dist/index.node.mjs
1.21KB -
plugin-core/dist/index.browser.mjs
1.18KB -
plugin-core/dist/index.native.mjs
1.18KB -
plugin-core/dist/index.node.mjs
1.18KB -
options/dist/index.browser.mjs
1.18KB -
options/dist/index.native.mjs
1.18KB -
options/dist/index.node.mjs
1.17KB -
accounts/dist/index.browser.mjs
1.17KB -
accounts/dist/index.native.mjs
1.17KB -
accounts/dist/index.node.mjs
1.16KB -
rpc-api/dist/index.browser.mjs
1.04KB -
rpc-api/dist/index.native.mjs
1.04KB -
rpc-api/dist/index.node.mjs
1.04KB -
compat/dist/index.browser.mjs
969B -
compat/dist/index.native.mjs
968B -
compat/dist/index.node.mjs
966B -
rpc-spec/dist/index.browser.mjs
928B -
rpc-spec/dist/index.native.mjs
928B -
rpc-spec/dist/index.node.mjs
926B -
rpc-subscriptions-api/dist/index.native.mjs
871B -
rpc-subscriptions-api/dist/index.browser.mjs
870B -
rpc-subscriptions-api/dist/index.node.mjs
870B -
promises/dist/index.native.mjs
841B -
promises/dist/index.node.mjs
840B -
promises/dist/index.browser.mjs
839B -
assertions/dist/index.browser.mjs
783B -
instructions/dist/index.browser.mjs
771B -
instructions/dist/index.native.mjs
770B -
instructions/dist/index.node.mjs
768B -
fast-stable-stringify/dist/index.browser.mjs
726B -
fast-stable-stringify/dist/index.native.mjs
725B -
assertions/dist/index.native.mjs
724B -
fast-stable-stringify/dist/index.node.mjs
724B -
assertions/dist/index.node.mjs
723B -
programs/dist/index.browser.mjs
329B -
programs/dist/index.native.mjs
327B -
programs/dist/index.node.mjs
325B -
fs-impl/dist/index.browser.mjs
245B -
event-target-impl/dist/index.node.mjs
230B -
functional/dist/index.browser.mjs
154B -
functional/dist/index.native.mjs
152B -
text-encoding-impl/dist/index.native.mjs
152B -
functional/dist/index.node.mjs
151B -
codecs/dist/index.browser.mjs
145B -
codecs/dist/index.native.mjs
144B -
codecs/dist/index.node.mjs
142B -
event-target-impl/dist/index.browser.mjs
133B -
ws-impl/dist/index.node.mjs
131B -
text-encoding-impl/dist/index.browser.mjs
122B -
fs-impl/dist/index.node.mjs
120B -
text-encoding-impl/dist/index.node.mjs
119B -
ws-impl/dist/index.browser.mjs
113B -
crypto-impl/dist/index.node.mjs
111B -
crypto-impl/dist/index.browser.mjs
109B -
rpc-parsed-types/dist/index.browser.mjs
66B -
rpc-parsed-types/dist/index.native.mjs
65B -
rpc-parsed-types/dist/index.node.mjs
63B -

Total files change -433B -0.08%

Final result: ✅

View report in BundleMon website ➡️


Current branch size history | Target branch size history

@mcintyre94

Copy link
Copy Markdown
Member Author

@trevor-cortex

@trevor-cortex trevor-cortex left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This PR adds the ClientWithTransactionSigning interface to @solana/plugin-interfaces, installing signTransaction(s) methods that mirror sendTransaction(s) — same flexible inputs, same config — but return the …WithOptionalSignature result types from #1893, since the fee payer signature may legitimately be absent. Docs pages, README, and typetests are all updated.

The implementation is solid:

  • The interface mirrors ClientWithTransactionSending faithfully — input unions, Config, and docblock structure all match the house style.
  • The typetests follow the repo's // [DESCRIBE] + nested-block convention, and the assertion that a ClientWithTransactionSending's methods structurally satisfy the signing methods is a nice way to pin down the required-signature-assignable-to-optional relationship.
  • README and both docs pages are updated consistently.

Two points before merge:

  1. Missing changeset. This ships a new public API on the published @solana/plugin-interfaces package, so per the repo conventions it needs a changeset (minor, generated via npx changeset add --empty). I don't see one in the changed files — if it's intentionally deferred to another PR in the stack, ignore this, but as-is this PR wouldn't trigger a release.

  2. context.transaction is optional on the results — see the inline comment. Not blocking, but worth a deliberate decision since handing back the transaction is the whole point of this interface.

For subsequent reviewers: the types this PR consumes are defined in #1893, so the main thing to verify here is the semantic contract (docblocks/README) rather than the type mechanics, which the typetests already cover.

Comment on lines +166 to +169
signTransaction: (
input: InstructionPlanInput | SingleTransactionPlan | SingleTransactionPlan['message'],
config?: Config,
) => Promise<SuccessfulSingleTransactionPlanResultWithOptionalSignature>;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Design question, non-blocking: SuccessfulSingleTransactionPlanResultWithOptionalSignature types context.transaction as optional (it comes from BaseTransactionPlanResultContext), yet retrieving the signed transaction is the entire purpose of this interface. As typed, every caller must handle result.context.transaction being undefined — including your own README example, which passes result.context.transaction straight to postToRelayer and would fail a strict type-check.

In practice successfulSingleTransactionPlanResultWithOptionalSignature always populates transaction, so the type is looser than the reality. Would it be worth tightening the return type here to guarantee transaction is present (e.g. intersecting with { context: { transaction: Transaction } }, or adding a dedicated result type in @solana/instruction-plans)? If the looseness is intentional — e.g. to allow executors that only report signatures — a sentence in the docblock explaining how callers should handle a missing transaction would help.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Agreed, changed this to narrow the context on this executor so that it has transaction: Transaction on the successful leaves.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Documentation Preview: https://kit-docs-mxfri30jp-anza-tech.vercel.app

@mcintyre94
mcintyre94 force-pushed the client-sign-transaction branch 2 times, most recently from 34963e8 to c18ec28 Compare August 7, 2026 13:01
@mcintyre94

Copy link
Copy Markdown
Member Author

@trevor-cortex

@trevor-cortex trevor-cortex left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks for addressing both points from the last pass — this now looks ready to go. (Would approve, but I don't have repository access to submit an approving review, so leaving this as a comment.)

What changed since my last review:

  • A minor changeset for @solana/plugin-interfaces was added, resolving point 1.
  • The optional-context.transaction concern (point 2) is resolved properly: the new SignedSingleTransactionPlanResult narrows SuccessfulSingleTransactionPlanResultWithOptionalSignature via the TContext slot so context.transaction is required, and SignedTransactionPlanResult threads that through the result tree for signTransactions. Neat use of the context generic — the intersection of transaction?: Transaction with { transaction: Transaction } does exactly the right thing, and it stays within the TSingle bound (SingleTransactionPlanResultWithOptionalSignature) used by the tree types.
  • The typetests now cover all the new guarantees: transaction always present, signature still optional, the loose result not assignable to the signing result, and — nicely — the flipped assertion that a ClientWithTransactionSending's methods no longer satisfy the signing methods, which documents the deliberate divergence introduced by the tightening.
  • README example and docblocks updated to match (result.context.transaction is now safe unguarded), and package.json/lockfile pick up the two new type-only workspace deps.

One non-blocking note (see inline): the successfulSingleTransactionPlanResultWithOptionalSignature factory in @solana/instruction-plans always populates transaction but its return type leaves it optional, so implementers building on that helper won't structurally satisfy SignedSingleTransactionPlanResult without a cast. Worth tightening upstream in a follow-up.

For subsequent reviewers: the type mechanics are fully pinned by the typetests; the remaining thing to sanity-check is the semantic contract in the docblocks/README (relayer use case, fee-payer-signature-optional semantics), which reads accurately to me.

Comment on lines +137 to +139
export type SignedSingleTransactionPlanResult = SuccessfulSingleTransactionPlanResultWithOptionalSignature<{
transaction: Transaction;
}>;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Non-blocking: the successfulSingleTransactionPlanResultWithOptionalSignature factory in @solana/instruction-plans always populates context.transaction, but its declared return type leaves it optional — so an executor implementation built on that helper won't structurally satisfy SignedSingleTransactionPlanResult without a cast or manual narrowing. Consider tightening the factory's return type upstream (in a follow-up PR in the stack, since that package isn't touched here) so implementations of this interface type-check out of the box.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This should now be resolved downstack: #1893 (comment)

@mcintyre94
mcintyre94 force-pushed the client-sign-transaction branch from c18ec28 to 601fc78 Compare August 7, 2026 14:50
@mcintyre94
mcintyre94 force-pushed the tx-plan-execute-missing-sig branch from a62ea89 to 58f44c2 Compare August 7, 2026 14:50
@mcintyre94
mcintyre94 force-pushed the client-sign-transaction branch from 601fc78 to e85b097 Compare August 7, 2026 15:06
@mcintyre94
mcintyre94 force-pushed the tx-plan-execute-missing-sig branch from 58f44c2 to 47d43e2 Compare August 7, 2026 15:06
@mcintyre94
mcintyre94 force-pushed the tx-plan-execute-missing-sig branch from 47d43e2 to e2e365c Compare August 7, 2026 15:52
@mcintyre94
mcintyre94 force-pushed the client-sign-transaction branch from e85b097 to ebdc059 Compare August 7, 2026 15:52
@mcintyre94
mcintyre94 requested a review from lorisleiva August 7, 2026 16:00
@mcintyre94
mcintyre94 marked this pull request as ready for review August 7, 2026 16:00
@mcintyre94
mcintyre94 removed the request for review from lorisleiva August 10, 2026 14:06
@mcintyre94
mcintyre94 marked this pull request as draft August 10, 2026 14:07
@mcintyre94
mcintyre94 changed the base branch from tx-plan-execute-missing-sig to graphite-base/1899 August 14, 2026 13:03
@mcintyre94
mcintyre94 force-pushed the client-sign-transaction branch from ebdc059 to 567e5e7 Compare August 14, 2026 13:03
@mcintyre94
mcintyre94 changed the base branch from graphite-base/1899 to tx-executor-context-only August 14, 2026 13:03
@mcintyre94
mcintyre94 force-pushed the client-sign-transaction branch from 567e5e7 to d5ce66b Compare August 14, 2026 14:18
@mcintyre94
mcintyre94 force-pushed the tx-executor-context-only branch from 0594be0 to 67cbc21 Compare August 14, 2026 14:18
…result contexts

`ClientWithTransactionSigning` provides `signTransaction` and `signTransactions`, which accept the same flexible inputs as their sending counterparts but hand back the signed transactions instead of submitting them. The interface is parameterised over the context attached to its results and makes no default guarantees about that context: what it contains is entirely decided by the plugin providing the capability. `ClientWithTransactionSending` gains the same `TContext` type parameter, but defaults it to `TransactionPlanResultContextWithSignature` for backward compatibility, so existing usage keeps the required `context.signature` on successful results.
@mcintyre94
mcintyre94 force-pushed the client-sign-transaction branch from d5ce66b to e9ff80a Compare August 14, 2026 15:56
@mcintyre94
mcintyre94 force-pushed the tx-executor-context-only branch from 67cbc21 to 2187418 Compare August 14, 2026 15:56
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