Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact 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

feat(bindings-ts)!: API change: return AssembledTransactions, require signAndSend, and support multi-auth workflows #1034

Merged
merged 1 commit into from
Dec 7, 2023

Conversation

chadoh
Copy link
Contributor

@chadoh chadoh commented Oct 19, 2023

Overview

Soroban, and Stellar more generally, supports having multiple parties sign the same transaction.

Prior to now, the libraries generated with soroban contract bindings typescript did not.

This adds multi-auth support to the generated libraries, streamlining it as much as possible while adding minimal overhead to the "normal stuff" that most people will do with these libraries 98% of the time.

Read calls (simulation only)

The biggest change for view calls, which require only a simulation and no signing, is that you now need to get the result of a transaction:

-const balance = await tokenContract.balance({ account: 'G…' })
+const balance = (await tokenContract.balance({ account: 'G…' })).result

You can streamline this further using destructuring:

-const balance = await tokenContract.balance({ account: 'G…' })
+const { result: balance } = await tokenContract.balance({ account: 'G…' })

Or just call the variable result and it looks very clean:

-const balance = await tokenContract.balance({ account: 'G…' })
+const { result } = await tokenContract.balance({ account: 'G…' })

Write calls with one signer

For these, you now need to explicitly sign and send the transaction. This helps you build up the mental model that the transaction is first simulated, that this is good enough for view calls, and that you also need a signAndSend step if you want a write call to actually go through.

-const result = await tokenContract.send({ to: 'G…', amount: 10n })
+const tx = await tokenContract.send({ to: 'G…', amount: 10n })
+const { result } = tx.signAndSend()

This also allows these libraries to expose information and options in more expectable places, rather than cluttering global interfaces. Examples:

  • tx.isReadCall – make explicit logic around read vs write calls
  • tx.signAndSend({ secondsToWait: 30 }) – specify how many seconds to wait for the transaction to complete (default: 10)
  • tx.raw – access the raw SorobanClient transaction that this object wraps
  • tx.simulationData – get the result of the simulation

Once you signAndSend(), the object you get back has different fields that are more relevant to the signing and sending behavior. See SentTransaction for more information.

Write calls with >1 signer

Let's consider an Atomic Swap contract. Alice wants to give 10 of her Token A tokens to Bob for 5 of his Token B tokens.

const ALICE = 'G123...'
const BOB = 'G456...'
const TOKEN_A = 'C123…'
const TOKEN_B = 'C456…'
const AMOUNT_A = 10n
const AMOUNT_b = 5n

Let's say Alice is also going to be the one signing the final transaction envelope, meaning she is the invoker. So your app, from Alice's browser, simulates the swap call:

const tx = await swapContract.swap({
  a: ALICE,
  b: BOB,
  token_a: TOKEN_A,
  token_b: TOKEN_B,
  amount_a: AMOUNT_A,
  amount_b: AMOUNT_B,
})

But your app can't signAndSend this right away, because Bob needs to sign it first. You can check this:

const whoElseNeedsToSign = tx.needsNonInvokerSigningBy()

You can verify that whoElseNeedsToSign is an array of length 1, containing only Bob's public key.

Then, still on Alice's machine, you can serialize the transaction-under-assembly:

const json = tx.toJSON()

And now you need to send it to Bob's browser. How you do this depends on your app. Maybe you send it to a server first, maybe you use WebSockets, or maybe you have Alice text the JSON blob to Bob and have him paste it into your app in his browser (note: this option might be error-prone 😄).

Once you get the JSON blob into your app on Bob's machine, you can deserialize it:

const tx = swapContract.fromJSON.swap(json)

And have Bob sign it. What Bob will actually need to sign is some auth entries within the transaction, not the transaction itself or the transaction envelope. Your app can verify that Bob has the correct wallet selected in Freighter, then:

await tx.signAuthEntries()

Under the hood, this uses signAuthEntry from the injected wallet (@stellar/freighter-api by default).

Now Bob can again serialize the transaction and send back to Alice, where she can finally signAndSend().

To see an even more complicated example, where Alice swaps with Bob but the transaction is invoked by yet another party, check out the new test-swap.ts

All the nitty gritty details

Here's a full(er) accounting of everything that changed here:

  • switch from ts-tests/package.json to an initialize.sh script that
    uses a .env if available or defaults to environment variables. As
    before, this will build, deploy, and generate bindings for the new
    contracts, plus creating an alice identity and minting separate
    amounts of two separate tokens to the root user and alice
  • add eslint to catch missing awaits
  • add transaction-rebuilding to increment transaction sequence numbers,
    so that all tests can pass when run in parallel
  • add atomic_swap and token contracts from
    https://github.com/stellar/soroban-examples to test-wasms
  • add tests for atomic swap functionality inspired by
    https://github.com/stellar/soroban-react-atomic-swap
  • let this logic guide needed updates to bindings typescript-generated
    libraries:
    • don't return flat values
    • instead, return a new AssembledTransaction, a class that has a
      result getter that can be used to get the result of the simulation
      right away, or can be used to await tx.signAndSend(), which then
      returns a SentTransaction
    • SentTransaction contains all possibly-relevant fields
      from the logic it performs, such as sendTransactionAll
    • AssembledTransaction has a needsNonInvokerSigningBy() getter
      and a signAuthEntriesFor(publicKey, signingFunction) to facilitate
      multi-auth workflows.
    • Since assembling transactions may now take place across multiple
      users' browsers, you can also call json = tx.toJSON() on an
      AssembledTransaction and then contract.fromJSON[method](json) on
      the next machine to continue signing, as shown in test-swap.ts

@chadoh chadoh force-pushed the feat/bindings-ts/multi-auth branch 2 times, most recently from 5898251 to 885955a Compare October 20, 2023 16:02
@chadoh chadoh force-pushed the feat/bindings-ts/multi-auth branch from 885955a to e69d1a9 Compare October 23, 2023 16:42
@chadoh chadoh force-pushed the feat/bindings-ts/multi-auth branch 5 times, most recently from 204476d to adf54a6 Compare October 26, 2023 18:14
@chadoh chadoh force-pushed the feat/bindings-ts/multi-auth branch from 360ed32 to d32b5a1 Compare November 1, 2023 15:33
@chadoh chadoh force-pushed the feat/bindings-ts/multi-auth branch from 561ffc4 to b49b8bb Compare November 7, 2023 17:38
@chadoh chadoh force-pushed the feat/bindings-ts/multi-auth branch 2 times, most recently from 4f630e4 to 4008a99 Compare November 7, 2023 20:19
@chadoh chadoh changed the base branch from main to release/v20.0.0-rc.4.1 November 7, 2023 20:20
@chadoh chadoh force-pushed the feat/bindings-ts/multi-auth branch 2 times, most recently from ede871b to 86bbd5b Compare November 8, 2023 19:16
@chadoh chadoh changed the base branch from release/v20.0.0-rc.4.1 to release/v20.0.0-rc.4.2 November 8, 2023 19:45
@chadoh chadoh force-pushed the feat/bindings-ts/multi-auth branch 9 times, most recently from 7fae2e1 to 6c68f73 Compare November 16, 2023 15:26
@kalepail
Copy link
Contributor

This is a special case, though!

Got it. Yeah I guess that makes sense. Unfortunate it's not more general but I doubt it'll be a major issue. Might be something worth looking into if it can't be reduced to just UX around current soroban-client functionality for multi-auth so you could use XDR vs special case JSON.

Maybe we rename it to tx.signAuthEntries

This makes sense to me and I see no downside to this

@aristidesstaffieri
Copy link
Contributor

  1. Is there a reason we're using JSON vs XDR for message passing? At the very least XDR should be supported even if we suggest JSON. The rest of the ecosystem uses XDR so we shouldn't special case that here imo.
  2. This example block of code seems awkward
import { signAuthEntry } from "@stellar/freighter-api";

await tx.signAuthEntriesFor(
  BOB,
  async payload => await signAuthEntry(payload, {
    accountToSign: BOB,
  })
)

Feels like a double sign, If you're signing with signAuthEntry what is signAuthEntriesFor doing other than applying that signature to the transaction? If that's the case why do we need to include the BOB account as an arg?

want to +1 this comment. I do think it would be better if clients did not have to work with structures that were specific to this lib and not compatible with other tooling(AssembledTransaction). IMO signers only need the auth entry xdr and they should be able to use any xdr tooling to parse it and sign it.

That said, I do realize that you can get to the underlying raw tx and this would be a big departure from the patterns here so I don't suggest any changes.

@chadoh chadoh force-pushed the feat/bindings-ts/multi-auth branch 5 times, most recently from 0d00195 to 9761d13 Compare November 30, 2023 16:14
@chadoh chadoh changed the title feat(bindings-ts): support multi-auth workflows feat(bindings-ts)!: API change: return AssembledTransactions, require signAndSend, and support multi-auth workflows Nov 30, 2023
@chadoh
Copy link
Contributor Author

chadoh commented Nov 30, 2023

IMO signers only need the auth entry xdr and they should be able to use any xdr tooling to parse it and sign it.

@aristidesstaffieri I agree with this assessment, and I think it's worth exploring how to simplify the auth-entry-signing workflow in a follow-up PR. For now, this one works! It's a big change and I'm eager to get it in so we can start building on top of it. Sound good?

@tyvdh @Shaptic @aristidesstaffieri I've addressed all feedback and updated the PR description. lmk what you think!

@chadoh chadoh force-pushed the feat/bindings-ts/multi-auth branch from 9761d13 to 1be4a60 Compare November 30, 2023 18:41
Copy link
Contributor

@kalepail kalepail left a comment

Choose a reason for hiding this comment

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

Again, haven't gone over the code but the interface lgtm

Copy link
Contributor

@Shaptic Shaptic left a comment

Choose a reason for hiding this comment

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

yeet it

@chadoh chadoh force-pushed the feat/bindings-ts/multi-auth branch 4 times, most recently from 0e1398b to 6e9d641 Compare December 6, 2023 19:04
@chadoh chadoh changed the base branch from release/v20.0.0-rc.4.2 to main December 6, 2023 19:05
@chadoh chadoh force-pushed the feat/bindings-ts/multi-auth branch 2 times, most recently from 15d7b26 to 2b888c8 Compare December 7, 2023 17:53
- switch from `ts-tests/package.json` to an `initialize.sh` script that
  uses a `.env` if available or defaults to environment variables. As
  before, this will build, deploy, and generate bindings for the new
  contracts, plus creating an `alice` and `bob` identities and minting
  separate amounts of two separate tokens to each of them
- add eslint to catch missing `await`s
- update to latest stellar-sdk instead of soroban-client
- add transaction-rebuilding to increment transaction sequence numbers,
  so that all tests can pass when run in parallel
- add `atomic_swap` and `token` contracts from
  https://github.com/stellar/soroban-examples to `test-wasms`
- add tests for atomic swap functionality inspired by
  https://github.com/stellar/soroban-react-atomic-swap
- let this logic guide needed updates to `bindings typescript`-generated
  libraries:
  - don't return flat values
  - instead, return a `new AssembledTransaction`, a class that has a
    `result` getter that can be used to get the result of the simulation
    right away, or can be used to `await tx.signAndSend()`, which then
    returns a `SentTransaction`
  - `SentTransaction` contains all possibly-relevant fields
    from the logic it performs, such as `sendTransactionAll`
  - `AssembledTransaction` has a `needsNonInvokerSigningBy()` getter
    and a `signAuthEntries()` to facilitate multi-auth workflows.
  - Since assembling transactions may now take place across multiple
    users' browsers, you can also call `json = tx.toJSON()` on an
    `AssembledTransaction` and then `contract.fromJSON[method](json)` on
    the next machine to continue signing, as shown in `test-swap.ts`

Co-authored-by: Aristides Staffieri <[email protected]>
Co-authored-by: George <[email protected]>
@chadoh chadoh force-pushed the feat/bindings-ts/multi-auth branch from 2b888c8 to 0ecb92d Compare December 7, 2023 17:54
@chadoh chadoh merged commit f24c449 into stellar:main Dec 7, 2023
20 of 21 checks passed
@chadoh chadoh deleted the feat/bindings-ts/multi-auth branch December 7, 2023 20:23
chadoh added a commit to AhaLabs/soroban-example-dapp that referenced this pull request Dec 18, 2023
_work in progress: needs CLI v20.0.3, which must actually include latest bindings logic. Somehow 20.0.2 does not, even though it was tagged post-multi-auth merge https://github.com/stellar/soroban-tools/commits/main/)._

- Pin soroban-cli to latest version, 20.0.3
- Update the contract's SDK version
  - **Copy in `abundance` source from `token` contract** at stellar/soroban-examples#277. Note that I **did not author most of the changes in the `contracts/abundance` folder**. I _only_ copied in the changes originally authored in the `soroban-examples` repo, then updated for the single tweak that this `abundance` contract already had: 1. comments on the `mint` method; 2. slightly modified `mint` behavior.
- New CLI's typescript-bindings-generated libraries now return an `AssembledTransaction`, which has a `result` getter and a `signAndSend` method, as explained in stellar/stellar-cli#1034
- Use `stellar-sdk` rather than `soroban-client`
chadoh added a commit to AhaLabs/soroban-example-dapp that referenced this pull request Dec 20, 2023
- Pin soroban-cli to latest version, 20.0.3
- Update the contract's SDK version
  - **Copy in `abundance` source from `token` contract** at stellar/soroban-examples#277. Note that I **did not author most of the changes in the `contracts/abundance` folder**. I _only_ copied in the changes originally authored in the `soroban-examples` repo, then updated for the single tweak that this `abundance` contract already had: 1. comments on the `mint` method; 2. slightly modified `mint` behavior.
- New CLI's typescript-bindings-generated libraries now return an `AssembledTransaction`, which has a `result` getter and a `signAndSend` method, as explained in stellar/stellar-cli#1034
- Use `stellar-sdk` rather than `soroban-client`
- Use latest `@stellar/freighter-api`
chadoh added a commit to AhaLabs/soroban-example-dapp that referenced this pull request Dec 20, 2023
- Pin soroban-cli to latest version, 20.1.0
- Update the contract's SDK version
  - **Copy in `abundance` source from `token` contract** at stellar/soroban-examples#277. Note that I **did not author most of the changes in the `contracts/abundance` folder**. I _only_ copied in the changes originally authored in the `soroban-examples` repo, then updated for the single tweak that this `abundance` contract already had: 1. comments on the `mint` method; 2. slightly modified `mint` behavior.
- New CLI's typescript-bindings-generated libraries now return an `AssembledTransaction`, which has a `result` getter and a `signAndSend` method, as explained in stellar/stellar-cli#1034
- Use `stellar-sdk` rather than `soroban-client`
- Use latest `@stellar/freighter-api`
chadoh added a commit to AhaLabs/soroban-example-dapp that referenced this pull request Dec 21, 2023
- Pin soroban-cli to latest version, 20.1.0
- Update the contract's SDK version
  - **Copy in `abundance` source from `token` contract** at stellar/soroban-examples#277. Note that I **did not author most of the changes in the `contracts/abundance` folder**. I _only_ copied in the changes originally authored in the `soroban-examples` repo, then updated for the single tweak that this `abundance` contract already had: 1. comments on the `mint` method; 2. slightly modified `mint` behavior.
- New CLI's typescript-bindings-generated libraries now return an `AssembledTransaction`, which has a `result` getter and a `signAndSend` method, as explained in stellar/stellar-cli#1034
- Use `stellar-sdk` rather than `soroban-client`
- Use latest `@stellar/freighter-api`
chadoh added a commit to AhaLabs/soroban-example-dapp that referenced this pull request Jan 5, 2024
- Pin soroban-cli to latest version, 20.1.1
- Update the contract's SDK version
  - **Copy in `abundance` source from `token` contract** at stellar/soroban-examples#277. Note that I **did not author most of the changes in the `contracts/abundance` folder**. I _only_ copied in the changes originally authored in the `soroban-examples` repo, then updated for the single tweak that this `abundance` contract already had: 1. comments on the `mint` method; 2. slightly modified `mint` behavior.
- New CLI's typescript-bindings-generated libraries now return an `AssembledTransaction`, which has a `result` getter and a `signAndSend` method, as explained in stellar/stellar-cli#1034
- Use `stellar-sdk` rather than `soroban-client`
- Use latest `@stellar/freighter-api`
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.

6 participants