solana-labs / solana-web3.js

Solana JavaScript SDK
https://solana-labs.github.io/solana-web3.js
MIT License
2.07k stars 830 forks source link

it should be possible to create a transaction using plain objects #1693

Open mikemaccana opened 10 months ago

mikemaccana commented 10 months ago

Motivation

Piping and method chaining are not common approach to configuration in JS SDKs.

It should be possible to create a transaction using Plain Old JavaScript Objects:

Before

// Create a transaction.
const transaction = pipe(
  createTransaction({ version: 0 }),
  (transaction) =>
    appendTransactionInstruction(
      getTransferInstruction(
        publicKey,
        destinationAddress,
        lamports(10_000_000n)
      ),
      transaction
    ),
  (transaction) => setTransactionFeePayer(publicKey, transaction),
  (transaction) =>
    setTransactionLifetimeUsingBlockhash(latestBlockhash, transaction)
);

Proposing

// Create a transaction.
const transaction = createTransaction({ 
  version: 0 
  instructions: [
    createInstruction(
      senderAddress,
      destinationAddress,
      lamports(10_000_000n)
    )
  ]
  feePayer: senderAddress,
  latestBlockhash
})

Example use case

A JS user, familiar with common JS practices of specifying config as a plain JS Object or TS Record<string, unknown> makes a transaction with createTransaction(). They know transactions have a series of instructions, so look for the instructions key find it. They add an array instructions as the value.

buffalojoec commented 10 months ago

I guess it would be totally possible to just use pipe under the hood here, but the resulting object would still be frozen like we're doing with each of those modular transaction modifiers.

We could roll something along these lines:

export function createTransaction(config: {
   feePayer?: Base58EncodedAddress,
   lifetime?: BlockhashLifetime | DurableNonceLifetime,
   instructions?: TransactionInstruction[],
   version: 'legacy' | 0,
}): Transaction | Transaction & TransactionWithFeePayer | Transaction & TransactionWithFeePayer & TransactionWithBlockhashLifetime {
   if 'feePayer' in config {
      pipe(
         createTransactionInner({ version: config.version }),
         tx => setTransactionFeePayer(config.feePayer, tx),
      )
   } else if {
      /* all the rest */
   }
}
mikemaccana commented 9 months ago

Pardon the wait, it's been a long month with Breakpoint

The intention is that the result not be frozen - rather that the transaction is undateable using standard JS mechanisms (object and array accessors) until the moment the transaction is sent.

Signing, building a JSON-RPC body and sending would be handled as an atomic operation after the developer considers the transaction 'complete', ie, so the signature always matches the transaction body. For example a developer could add a instruction by using array methods they already know, or set values inside the transaction using object.key accessors.

When the developer wants to sign and send the transaction, they run const txID = await signAndSendTransaction(transaction) and:

buffalojoec commented 9 months ago

Thanks for circling back @mikemaccana, I think @lorisleiva has just the thing for you on this. You can see glimpse of how this is going to look with the new signers API in #1792.

TL/DR: If you want short-hand, you'll do something like createDefaultTransaction(instructions) where instructions have actual signers in them as IAccountMeta, and then you'll signSendAndConfirmTransaction(..).

So it's coming along nicely!