8000 Allow messages with no lifetime to be compiled by lorisleiva · Pull Request #581 · anza-xyz/kit · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Allow messages with no lifetime to be compiled #581

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact 8000 its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

lorisleiva
Copy link
Member
@lorisleiva lorisleiva commented Jun 26, 2025

This PR removes the lifetime requirements on functions that compile transaction messages into transactions.

That is:

  • A TransactionMessage with no lifetime will return a Transaction without the TransactionWithLifetime flag. However, the compiled bytes will contain 32 zeroes where the lifetime constraint is meant to be stored. (new behaviour)
  • A TransactionMessage with an lifetime such as TransactionMessageWithBlockhashLifetime will be forwarded to the returned Transaction type — e.g. TransactionWithBlockhashLifetime. (existing behaviour)
  • A TransactionMessage with a loose lifetime — i.e. TransactionMessageWithLifetime — will be forwarded to the returned TransactionType — i.e. TransactionWithLifetime. (existing behaviour).

Copy link
changeset-bot bot commented Jun 26, 2025

🦋 Changeset detected

Latest commit: ccad1a3

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

This PR includes changesets to release 40 packages
Name Type
@solana/transaction-messages Minor
@solana/transactions Minor
@solana/signers Minor
@solana/kit Minor
@solana/programs Minor
@solana/rpc-api Minor
@solana/rpc-subscriptions-api Minor
@solana/transaction-confirmation Minor
@solana/compat Minor
@solana/react Minor
@solana/rpc-graphql Minor
@solana/rpc Minor
@solana/sysvars Minor
@solana/rpc-subscriptions 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/errors Minor
@solana/fast-stable-stringify Minor
@solana/functional Minor
@solana/instructions Minor
@solana/keys Minor
@solana/nominal-types Minor
@solana/options Minor
@solana/promises Minor
@solana/rpc-parsed-types Minor
@solana/rpc-spec-types Minor
@solana/rpc-spec Minor
@solana/rpc-subscriptions-channel-websocket Minor
@solana/rpc-subscriptions-spec Minor
@solana/rpc-transformers Minor
@solana/rpc-transport-http Minor
@solana/rpc-types Minor
@solana/subscribable 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

Copy link
Member Author
lorisleiva commented Jun 26, 2025

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

Copy link
bundlemon bot commented Jun 26, 2025

BundleMon

Files updated (7)
Status Path Size Limits
transaction-messages/dist/index.browser.mjs
7.29KB (+50B +0.67%) -
transaction-messages/dist/index.native.mjs
7.29KB (+50B +0.67%) -
transaction-messages/dist/index.node.mjs
7.29KB (+49B +0.66%) -
transactions/dist/index.browser.mjs
2.3KB (+32B +1.38%) -
transactions/dist/index.native.mjs
2.3KB (+32B +1.38%) -
transactions/dist/index.node.mjs
2.29KB (+31B +1.34%) -
@solana/kit production bundle
kit/dist/index.production.min.js
34.5KB (+29B +0.08%) -
Unchanged files (120)
Status Path Size Limits
rpc-graphql/dist/index.browser.mjs
18.82KB -
rpc-graphql/dist/index.native.mjs
18.82KB -
rpc-graphql/dist/index.node.mjs
18.82KB -
errors/dist/index.node.mjs
14.59KB -
errors/dist/index.browser.mjs
14.57KB -
errors/dist/index.native.mjs
14.57KB -
codecs-data-structures/dist/index.native.mjs
4.77KB -
codecs-data-structures/dist/index.browser.mjs
4.77KB -
codecs-data-structures/dist/index.node.mjs
4.77KB -
webcrypto-ed25519-polyfill/dist/index.node.mj
s
3.57KB -
webcrypto-ed25519-polyfill/dist/index.browser
.mjs
3.56KB -
webcrypto-ed25519-polyfill/dist/index.native.
mjs
3.54KB -
rpc-subscriptions/dist/index.browser.mjs
3.38KB -
rpc-subscriptions/dist/index.node.mjs
3.34KB -
rpc-subscriptions/dist/index.native.mjs
3.31KB -
codecs-core/dist/index.browser.mjs
3.3KB -
codecs-core/dist/index.native.mjs
3.3KB -
codecs-core/dist/index.node.mjs
3.3KB -
addresses/dist/index.browser.mjs
2.93KB -
rpc-transformers/dist/index.browser.mjs
2.93KB -
rpc-transformers/dist/index.native.mjs
2.93KB -
addresses/dist/index.native.mjs
2.93KB -
addresses/dist/index.node.mjs
2.93KB -
rpc-transformers/dist/index.node.mjs
2.93KB -
kit/dist/index.browser.mjs
2.71KB -
kit/dist/index.native.mjs
2.71KB -
kit/dist/index.node.mjs
2.71KB -
signers/dist/index.browser.mjs
2.63KB -
signers/dist/index.native.mjs
2.63KB -
signers/dist/index.node.mjs
2.62KB -
codecs-strings/dist/index.browser.mjs
2.53KB -
codecs-strings/dist/index.node.mjs
2.48KB -
codecs-strings/dist/index.native.mjs
2.45KB -
transaction-confirmation/dist/index.node.mjs
2.39KB -
sysvars/dist/index.browser.mjs
2.35KB -
sysvars/dist/index.native.mjs
2.34KB -
transaction-confirmation/dist/index.native.mj
s
2.34KB -
sysvars/dist/index.node.mjs
2.34KB -
transaction-confirmation/dist/index.browser.m
js
2.34KB -
rpc-subscriptions-spec/dist/index.node.mjs
2.18KB -
rpc-subscriptions-spec/dist/index.native.mjs
2.13KB -
rpc-subscriptions-spec/dist/index.browser.mjs
2.13KB -
keys/dist/index.browser.mjs
2.02KB -
keys/dist/index.native.mjs
2.02KB -
keys/dist/index.node.mjs
2.02KB -
codecs-numbers/dist/index.native.mjs
2.01KB -
codecs-numbers/dist/index.browser.mjs
2.01KB -
codecs-numbers/dist/index.node.mjs
2.01KB -
react/dist/index.native.mjs
1.99KB -
react/dist/index.browser.mjs
1.99KB -
react/dist/index.node.mjs
1.99KB -
rpc/dist/index.node.mjs
1.95KB -
rpc-transport-http/dist/index.browser.mjs
1.91KB -
rpc-transport-http/dist/index.native.mjs
1.91KB -
rpc/dist/index.native.mjs
1.8KB -
subscribable/dist/index.node.mjs
1.8KB -
rpc/dist/index.browser.mjs
1.8KB -
subscribable/dist/index.native.mjs
1.75KB -
subscribable/dist/index.browser.mjs
1.74KB -
rpc-transport-http/dist/index.node.mjs
1.73KB -
rpc-types/dist/index.browser.mjs
1.53KB -
rpc-types/dist/index.native.mjs
1.53KB -
rpc-types/dist/index.node.mjs
1.53KB -
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 -
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.13KB -
accounts/dist/index.native.mjs
1.12KB -
accounts/dist/index.node.mjs
1.12KB -
compat/dist/index.browser.mjs
971B -
compat/dist/index.native.mjs
970B -
compat/dist/index.node.mjs
968B -
rpc-spec-types/dist/index.browser.mjs
964B -
rpc-api/dist/index.browser.mjs
963B -
rpc-api/dist/index.native.mjs
962B -
rpc-spec-types/dist/index.native.mjs
962B -
rpc-api/dist/index.node.mjs
961B -
rpc-spec-types/dist/index.node.mjs
961B -
rpc-subscriptions-api/dist/index.native.mjs
870B -
rpc-subscriptions-api/dist/index.node.mjs
869B -
rpc-subscriptions-api/dist/index.browser.mjs
868B -
rpc-spec/dist/index.browser.mjs
852B -
rpc-spec/dist/index.native.mjs
851B -
rpc-spec/dist/index.node.mjs
850B -
promises/dist/index.browser.mjs
799B -
promises/dist/index.native.mjs
798B -
promises/dist/index.node.mjs
797B -
assertions/dist/index.browser.mjs
783B -
instructions/dist/index.browser.mjs
769B -
instructions/dist/index.native.mjs
768B -
instructions/dist/index.node.mjs
767B -
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 -
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
137B -
codecs/dist/index.native.mjs
136B -
codecs/dist/index.node.mjs
134B -
event-target-impl/dist/index.browser.mjs
133B -
ws-impl/dist/index.node.mjs
131B -
text-encoding-impl/dist/index.browser.mjs
122B -
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 +273B +0.08%

Final result: ✅

View report in BundleMon website ➡️


Current branch size history | Target branch size history

@lorisleiva lorisleiva force-pushed the 06-26-allow_messages_with_no_lifetime_to_be_compiled branch from ef66fd3 to 01ec4de Compare June 26, 2025 10:04
Copy link
Contributor
github-actions bot commented Jun 26, 2025

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

@lorisleiva lorisleiva force-pushed the 06-25-add_transactionwithlifetime_requirement_to_signing_functions branch from f9a39f0 to 01380d2 Compare June 26, 2025 10:14
@lorisleiva lorisleiva force-pushed the 06-26-allow_messages_with_no_lifetime_to_be_compiled branch 2 times, most recently from 4d50941 to e943aea Compare June 26, 2025 10:17
@lorisleiva lorisleiva marked this pull request as ready for review June 26, 2025 10:25
Copy link
Member
@mcintyre94 mcintyre94 left a comment

Choose a reason for hiding this comment

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

Nice, this looks great!

@@ -216,7 +216,7 @@ export type DecompileTransactionMessageConfig = {
export function decompileTransactionMessage(
compiledTransactionMessage: CompiledTransactionMessage,
config?: DecompileTransactionMessageConfig,
): CompilableTransactionMessage {
): BaseTransactionMessage & TransactionMessageWithFeePayer & TransactionMessageWithLifetime {
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure about returning TransactionMessageWithLifetime here. My concern is that if you compile a message without a lifetime, and then decompile it, should we say it now has a lifetime?

Maybe, it's going to have an all-zero blockhash in the lifetimeConstraint field. But I wonder if it'd be better to make apps narrow to TransactionMessageWithLifetime themselves after doing their own checks.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah this is the part where I don't think either solution is ideal.

The reality is, whenever we decompile, we always fill in the lifetimeConstraint property of the transaction message, even if it's all zeros. So structurally we do always return a TransactionMessageWithLifetime.

However, you are right that it feels weird that compiling/decompiling a message magically introduces the TransactionMessageWithLifetime flag.

But, decompiling a message will typically be used to find information about a transaction that landed and therefore it would be a shame to make apps always use isTransactionMessageWithBlockhashLifetime and isTransactionMessageWithDurableNonceLifetime when the data is technically always there.

My interpretation of this is kinda like the From and To type parameters of Codecs. From is allowed to be looser than To because the encoder can fill some gaps. However, decoding is stricter because we cannot fill any gaps if information is missing from the provided bytes.

I'm happy with either solution though.

Copy link
Member

Choose a reason for hiding this comment

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

I think you're probably right. There are use cases like wallets and relayers where you're dealing with serialized transactions that haven't been landed, but that is a minority and those apps need to be more sophisticated anyway. The comparison to codecs makes sense too.

< 8000 /div>

@lorisleiva lorisleiva force-pushed the 06-26-allow_messages_with_no_lifetime_to_be_compiled branch 2 times, most recently from 73044fd to a735334 Compare June 26, 2025 15:29
@lorisleiva lorisleiva force-pushed the 06-25-add_transactionwithlifetime_requirement_to_signing_functions branch from 01380d2 to f1998fe Compare June 26, 2025 15:29
Copy link
Collaborator
@steveluscher steveluscher left a comment

Choose a reason for hiding this comment

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

Added via Giphy

@@ -46,6 +49,11 @@ type VersionedCompiledTransactionMessage = BaseCompiledTransactionMessage &
version: number;
}>;

const EMPTY_BLOCKHASH_LIFETIME_CONSTRAINT = {
blockhash: '11111111111111111111111111111111',
lastValidBlockHeight: 0n,
Copy link
Collaborator

Choose a reason for hiding this comment

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

I know that getCompiledLifetimeToken() ignores this value so it can be literally anything, but this is maybe confusing to the reader (ie. that it's here).

We could obviate the need for this by doing…

const LIFETIME_TOKEN_WHEN_LIFETIME_ABSENT = '11111111111111111111111111111111';

/* ... */
    lifetimeToken: transactionMessage.lifetimeConstraint
        ? getCompiledLifetimeToken(transactionMessage.lifetimeConstraint)
        : LIFETIME_TOKEN_WHEN_LIFETIME_ABSENT,

return {
...(transactionMessage.version !== 'legacy'
? { addressTableLookups: getCompiledAddressTableLookups(orderedAccounts) }
: null),
header: getCompiledMessageHeader(orderedAccounts),
instructions: getCompiledInstructions(transactionMessage.instructions, orderedAccounts),
lifetimeToken: getCompiledLifetimeToken(transactionMessage.lifetimeConstraint),
lifetimeToken: getCompiledLifetimeToken(lifetimeConstraint),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Honestly, I thought we'd just make this optional on CompiledTransactionMessage and then make the compiler (ie. getCompiledTransactionMessageEncoder()) encapsulate the behaviour of filling it in when absent.

Actually I think I like that better, because then we're not passing a value with a "11111111111111111111111111111111" in it around your app.

Copy link
Collaborator
@steveluscher steveluscher left a comment

Choose a reason for hiding this comment

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

Yeah, actually maybe I take that back.

There's two levels of compiling here:

  1. From a nicely formatted message to the unreadable garbage with account indexes and stuff (ie. compileTransactionMessage())
  2. From the garbage format to wire format (ie. getCompiledTransactionMessageEncoder())

I think the behaviour of ‘fill it in with zeros’ should be done at the very latest opportunity – the one where we no longer have a choice. The higher level data structure should just not have a lifetimeToken property if there's no lifetime, rather than to fill it in with a placeholder.

@lorisleiva lorisleiva force-pushed the 06-26-allow_messages_with_no_lifetime_to_be_compiled branch from a735334 to 5c49737 Compare June 27, 2025 10:00
@lorisleiva lorisleiva force-pushed the 06-25-add_transactionwithlifetime_requirement_to_signing_functions branch from f1998fe to 42bc80f Compare June 27, 2025 10:00
@lorisleiva
Copy link
Member Author
lorisleiva commented Jun 27, 2025

@steveluscher Hmm implementing this is making me doubt this is the right decision. It makes the whole "what happens when we decompile" problem even deeper — unless we recognise "zeroes" as an empty sigil which you said you'd rather avoid.

Consider the following encoding (A) and decoding (B) paths.

A) TransactionMessage → CompiledTransactionMessage → Bytes
B) Bytes → CompiledTransactionMessage → TransactionMessage

When we don't have a a lifetime, we get the following back and forth:

  • A) TransactionMessage with no lifetime → CompiledTransactionMessage with no lifetime → Bytes filled with zeroes
  • B) Bytes filled with zeroes → CompiledTransactionMessage always with a blockhash lifetime even though the attribute is optional!TransactionMessage with blockhash lifetime.

I know we currently have this issue anyway, that is:

  • A) TransactionMessage with no lifetime → CompiledTransactionMessage with blockhash lifetime → Bytes filled with zeroes
  • B) Bytes filled with zeroes → CompiledTransactionMessage with blockhash lifetime → TransactionMessage with blockhash lifetime.

But I found there is something weird about optionalising the lifetime attribute of the CompiledTransactionMessage — which is the low-level / close-to-the-actual-bytes format. It breaks away from the From/To analogy of codecs I was mentioning to Callum. We could introduce another type for path (B) but the only difference would be that the lifetimeConstraint attribute will be required.

What do you think?

P.S.: I submitted a PR that tries this strategy but with a zero sigil to avoid the issue above. Lmk what you think.

@mcintyre94
Copy link
Member

Taking a step back, do we actually need to be able to get to CompiledTransactionMessage without a lifetime?

The use case mentioned was if you have a transaction modifying signer, it might add a lifetime. So you want to be able to compile without having one so the signer can take care of it. We also know there are cases where an app might use specific logic to 'fake' its lifetime, but because of the limitation of the bytecode format it does have a lifetime when compiled.

Can we differentiate the signer case further here?

If I'm just using compileTransactionMessage then it still requires a lifetime, because it's just a compile step. Whatever lifetime the transaction message has ends up in the CompiledTransactionMessage. If the app wants to special-case the lifetime because it's going to do something else after serializing, that's fine, but it's just app logic. It wants to compile so it sets some lifetime.

But if I'm using signers then I can enter the compile flow without a lifetime. The transaction modifying signers get their chance to modify the transaction, and one of them might add a lifetime. Maybe it's an app-specific special case, maybe it's a real one. But if they add a lifetime then great, that ends up in the CompiledTransactionMessage. If none of them do, then we throw an error because you can't actually go to CompiledTransactionMessage without some lifetime. Either your app or a signer must add one.

The design challenge here is the signer interface operates on Transaction, but I wonder if that's where we should be looking to address this use case.

@lorisleiva
Copy link
Member Author

Hey @mcintyre94, I'm not sure what you are suggesting about the Signer API, but I believe this was tackled on this PR. Unless you had something different in mind?

Regarding the lack of lifetimeToken in CompiledTransactionMessage, I also personally don't find this change ideal because a compiled message always has a dedicated 32-bytes slot for that token. It feels weird to me to make this optional only to push the "fill with zeroes" logic at the Codec level. I did however implement this suggestion here so we can see the consequences of that change explicitly rather than speculate on it. ☺️

@mcintyre94
Copy link
Member

I think my suggestion would be a larger change to signTransactionMessageWithSigners, but nothing else would change. CompiledTransactionMessage still requires lifetime token, Kit has no logic to default it anywhere, compileTransactionMessage still requires a lifetime.

I think the only case where this is really necessary is when you're using signers, and a signer is going to modify the transaction. Anything else, like excluding the lifetime for Solana Pay, is app-specific logic IMO.

So I think you could make a signer method something like:

  • For each modifying signer, modify the transaction message
  • Assert it has a lifetime after this point, throw an error if it does not
  • Compile the resulting transaction message using an unchanged compileTransactionMessage
  • Sign the transaction for all signers

The concrete API change would be to signers, a modifying signer would need to have a function with the signature T extends TransactionMessage -> Promise<U extends TransactionMessage> at this step. Instead of only T extends Transaction -> Promise<T>.

@lorisleiva
Copy link
Member Author

@mcintyre94 Oof this is a very different direction haha.

I don't particularly agree with your premise that this is only something only the Signer API must handle. There are valid use cases that require apps to compile transaction messages before knowing which lifetime they are going to use. For instance, if they want to figure out the size of that transaction message to figure out if they can fit more instructions into it. Or anything that they could do with the Signer API — e.g. sending one to a wallet — but directly with the wallet's API.

I do believe there is a strong need for compileTransactionMessage to accept a TransactionMessage with no lifetime. This is what started this whole journey and is a blocker to InstructionPlans. The main question here is: how far do we want to push that lifeless border. Personally, I'd recommend stopping at CompiledTransactionMessage since it clearly requires 32 bytes for a lifetimeToken.

Regarding the suggested TransactionModifyingSigner change, I don't think it is viable to use a signature such as T extends TransactionMessage -> Promise<U extends TransactionMessage> for them. TransactionModifyingSigners could technically be stacked one after the other meaning we must use the Transaction API in order to avoid having to re-compile each time we sign. More importantly, the TransactionMessage API doesn't store any signature dictionary which is needed for TransactionSigners to do their job. They ultimately just fill that signature directory with one or more signature based on the type of wallet they are.

@lorisleiva lorisleiva force-pushed the 06-25-add_transactionwithlifetime_requirement_to_signing_functions branch from 42bc80f to 639657c Compare July 1, 2025 09:22
@lorisleiva lorisleiva force-pushed the 06-26-allow_messages_with_no_lifetime_to_be_compiled branch from 5c49737 to 2d89a8a Compare July 1, 2025 09:22
@lorisleiva
Copy link
Member Author

@steveluscher I'm not sure if you noticed but the next PR of this stack now tackles the changes we discussed. Lmk if that's also what you expected. 🙏

Copy link
Collaborator
@steveluscher steveluscher left a comment

Choose a reason for hiding this comment

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

Added via Giphy

@lorisleiva lorisleiva force-pushed the 06-25-add_transactionwithlifetime_requirement_to_signing_functions branch from 639657c to fc3920b Compare July 9, 2025 08:13
@lorisleiva lorisleiva force-pushed the 06-26-allow_messages_with_no_lifetime_to_be_compiled branch from 2d89a8a to 5f77a74 Compare July 9, 2025 08:13
Copy link
Member Author
lorisleiva commented Jul 9, 2025

Merge activity

  • Jul 9, 8:32 AM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Jul 9, 8:33 AM UTC: Graphite rebased this pull request as part of a merge.
  • Jul 9, 8:36 AM UTC: @lorisleiva merged this pull request with Graphite.

@lorisleiva lorisleiva changed the base branch from 06-25-add_transactionwithlifetime_requirement_to_signing_functions to graphite-base/581 July 9, 2025 08:32
@lorisleiva lorisleiva changed the base branch from graphite-base/581 to main July 9, 2025 08:32
@lorisleiva lorisleiva force-pushed the 06-26-allow_messages_with_no_lifetime_to_be_compiled branch from 5f77a74 to ccad1a3 Compare July 9, 2025 08:33
@lorisleiva lorisleiva merged commit 55d6b04 into main Jul 9, 2025
14 checks passed
@lorisleiva lorisleiva deleted the 06-26-allow_messages_with_no_lifetime_to_be_compiled branch July 9, 2025 08:36
@github-actions github-actions bot mentioned this pull request Jul 9, 2025
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.

3 participants
0