Skip to content

Commit

Permalink
Update signTransaction and signAuthEntries to match SEP-43 (#1097)
Browse files Browse the repository at this point in the history
* add documentation for new args
* modify basic node signer to match new Sep43 spec
* add changelog, proper error type for basic node signer

---------

Co-authored-by: Chad Ostrowski <[email protected]>
  • Loading branch information
BlaineHeffron and chadoh authored Nov 13, 2024
1 parent 94e1ce8 commit 40f2175
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 34 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ A breaking change will get clearly marked in this log.

## Unreleased

### Breaking Changes
- The `ClientOptions.signTransaction` type has been updated to reflect the latest [SEP 43](https://github.com/stellar/stellar-protocol/blob/eb401f932258c827a5b4a2e14aea939affcd2b02/ecosystem/sep-0043.md#wallet-interface-format) protocol, which matches the latest major version of Freighter and other wallets. It now accepts `address`, `submit`, and `submitUrl` options, and it returns a promise containing the `signedTxXdr` and the `signerAddress`. It now also returns an `Error` type if an error occurs during signing.
* `basicNodeSigner` has been updated to reflect the new type.
- `ClientOptions.signAuthEntry` type has also been updated to reflect the SEP 43 protocol, which also returns a promise containing the`signerAddress` in addition to the `signAuthEntry` that was returned previously. It also can return an `Error` type.

### Added
- `stellartoml-Resolver.resolve` now has a `allowedRedirects` option to configure the number of allowed redirects to follow when resolving a stellar toml file.

Expand Down
72 changes: 59 additions & 13 deletions src/contract/assembled_transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
ClientOptions,
MethodOptions,
Tx,
WalletError,
XDR_BASE64,
} from "./types";
import { Server } from "../rpc";
Expand Down Expand Up @@ -324,6 +325,10 @@ export class AssembledTransaction<T> {
NotYetSimulated: class NotYetSimulatedError extends Error { },
FakeAccount: class FakeAccountError extends Error { },
SimulationFailed: class SimulationFailedError extends Error { },
InternalWalletError: class InternalWalletError extends Error { },
ExternalServiceError: class ExternalServiceError extends Error { },
InvalidClientRequest: class InvalidClientRequestError extends Error { },
UserRejected: class UserRejectedError extends Error { },
};

/**
Expand Down Expand Up @@ -416,6 +421,26 @@ export class AssembledTransaction<T> {
return txn;
}

private handleWalletError(error?: WalletError): void {
if (!error) return;

const { message, code } = error;
const fullMessage = `${message}${error.ext ? ` (${ error.ext.join(', ') })` : ''}`;

switch (code) {
case -1:
throw new AssembledTransaction.Errors.InternalWalletError(fullMessage);
case -2:
throw new AssembledTransaction.Errors.ExternalServiceError(fullMessage);
case -3:
throw new AssembledTransaction.Errors.InvalidClientRequest(fullMessage);
case -4:
throw new AssembledTransaction.Errors.UserRejected(fullMessage);
default:
throw new Error(`Unhandled error: ${fullMessage}`);
}
}

private constructor(public options: AssembledTransactionOptions<T>) {
this.options.simulate = this.options.simulate ?? true;
this.server = new Server(this.options.rpcUrl, {
Expand Down Expand Up @@ -683,13 +708,21 @@ export class AssembledTransaction<T> {
.setTimeout(timeoutInSeconds)
.build();

const signature = await signTransaction(
const signOpts: Parameters<NonNullable<ClientOptions['signTransaction']>>[1] = {
networkPassphrase: this.options.networkPassphrase,
};

if (this.options.address) signOpts.address = this.options.address;
if (this.options.submit !== undefined) signOpts.submit = this.options.submit;
if (this.options.submitUrl) signOpts.submitUrl = this.options.submitUrl;

const { signedTxXdr: signature, error } = await signTransaction(
this.built.toXDR(),
{
networkPassphrase: this.options.networkPassphrase,
},
signOpts,
);

this.handleWalletError(error);

this.signed = TransactionBuilder.fromXDR(
signature,
this.options.networkPassphrase,
Expand Down Expand Up @@ -728,7 +761,20 @@ export class AssembledTransaction<T> {
signTransaction?: ClientOptions["signTransaction"];
} = {}): Promise<SentTransaction<T>> => {
if(!this.signed){
await this.sign({ force, signTransaction });
// Store the original submit option
const originalSubmit = this.options.submit;

// Temporarily disable submission in signTransaction to prevent double submission
if (this.options.submit) {
this.options.submit = false;
}

try {
await this.sign({ force, signTransaction });
} finally {
// Restore the original submit option
this.options.submit = originalSubmit;
}
}
return this.send();
};
Expand Down Expand Up @@ -841,7 +887,7 @@ export class AssembledTransaction<T> {
* If you have a pro use-case and need to override the default `authorizeEntry` function, rather than using the one in @stellar/stellar-base, you can do that! Your function needs to take at least the first argument, `entry: xdr.SorobanAuthorizationEntry`, and return a `Promise<xdr.SorobanAuthorizationEntry>`.
*
* Note that you if you pass this, then `signAuthEntry` will be ignored.
*/
*/
authorizeEntry?: typeof stellarBaseAuthorizeEntry;
} = {}): Promise<void> => {
if (!this.built)
Expand Down Expand Up @@ -898,13 +944,13 @@ export class AssembledTransaction<T> {
// eslint-disable-next-line no-await-in-loop
authEntries[i] = await authorizeEntry(
entry,
async (preimage) =>
Buffer.from(
await sign(preimage.toXDR("base64"), {
accountToSign: address,
}),
"base64",
),
async (preimage) => {
const { signedAuthEntry, error } = await sign(preimage.toXDR("base64"), {
address,
});
this.handleWalletError(error);
return Buffer.from(signedAuthEntry, "base64");
},
await expiration, // eslint-disable-line no-await-in-loop
this.options.networkPassphrase,
);
Expand Down
26 changes: 19 additions & 7 deletions src/contract/basic_node_signer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Keypair, TransactionBuilder, hash } from "@stellar/stellar-base";
import type { Client } from "./client";
import { SignAuthEntry, SignTransaction } from "./types";

/**
* For use with {@link Client} and {@link module:contract.AssembledTransaction}.
Expand All @@ -16,14 +17,25 @@ import type { Client } from "./client";
export const basicNodeSigner = (
keypair: Keypair,
networkPassphrase: string,
) => ({
): {
signTransaction: SignTransaction;
signAuthEntry: SignAuthEntry;
} => ({
// eslint-disable-next-line require-await
signTransaction: async (tx: string) => {
const t = TransactionBuilder.fromXDR(tx, networkPassphrase);
signTransaction: async (xdr, opts) => {
const t = TransactionBuilder.fromXDR(xdr, opts?.networkPassphrase || networkPassphrase);
t.sign(keypair);
return t.toXDR();
return {
signedTxXdr: t.toXDR(),
signerAddress: keypair.publicKey(),
};
},
// eslint-disable-next-line require-await
signAuthEntry: async (entryXdr: string): Promise<string> =>
keypair.sign(hash(Buffer.from(entryXdr, "base64"))).toString("base64"),
});
signAuthEntry: async (authEntry) => {
const signedAuthEntry = keypair.sign(hash(Buffer.from(authEntry, "base64"))).toString("base64");
return {
signedAuthEntry,
signerAddress: keypair.publicKey(),
};
},
});
88 changes: 74 additions & 14 deletions src/contract/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,60 @@ export type Duration = bigint;
*/
export type Tx = Transaction<Memo<MemoType>, Operation[]>;

export interface WalletError {
message: string; // general description message returned to the client app
code: number; // unique error code
ext?: Array<string>; // optional extended details
}

/**
* A function to request a wallet to sign a built transaction
*
* This function takes an XDR provided by the requester and applies a signature to it.
* It returns a base64-encoded string XDR-encoded Transaction Envelope with Decorated Signatures
* and the signer address back to the requester.
*
* @param xdr - The XDR string representing the transaction to be signed.
* @param opts - Options for signing the transaction.
* @param opts.networkPassphrase - The network's passphrase on which the transaction is intended to be signed.
* @param opts.address - The public key of the account that should be used to sign.
* @param opts.submit - If set to true, submits the transaction immediately after signing.
* @param opts.submitUrl - The URL of the network to which the transaction should be submitted, if applicable.
*
* @returns A promise resolving to an object with the signed transaction XDR and optional signer address and error.
*/
export type SignTransaction = (xdr: string, opts?: {
networkPassphrase?: string;
address?: string;
submit?: boolean;
submitUrl?: string;
}) => Promise<{
signedTxXdr: string;
signerAddress?: string;
} & { error?: WalletError }>;

/**
* A function to request a wallet to sign an authorization entry preimage.
*
* Similar to signing a transaction, this function takes an authorization entry preimage provided by the
* requester and applies a signature to it.
* It returns a signed hash of the same authorization entry and the signer address back to the requester.
*
* @param authEntry - The authorization entry preimage to be signed.
* @param opts - Options for signing the authorization entry.
* @param opts.networkPassphrase - The network's passphrase on which the authorization entry is intended to be signed.
* @param opts.address - The public key of the account that should be used to sign.
*
* @returns A promise resolving to an object with the signed authorization entry and optional signer address and error.
*/
export type SignAuthEntry = (authEntry: string, opts?: {
networkPassphrase?: string;
address?: string;
}) => Promise<{
signedAuthEntry: string;
signerAddress?: string;
} & { error?: WalletError }>;

/**
* Options for a smart contract client.
* @memberof module:contract
Expand All @@ -77,14 +131,7 @@ export type ClientOptions = {
*
* Matches signature of `signTransaction` from Freighter.
*/
signTransaction?: (
tx: XDR_BASE64,
opts?: {
network?: string;
networkPassphrase?: string;
accountToSign?: string;
},
) => Promise<XDR_BASE64>;
signTransaction?: SignTransaction;
/**
* A function to sign a specific auth entry for a transaction, using the
* private key corresponding to the provided `publicKey`. This is only needed
Expand All @@ -94,12 +141,7 @@ export type ClientOptions = {
*
* Matches signature of `signAuthEntry` from Freighter.
*/
signAuthEntry?: (
entryXdr: XDR_BASE64,
opts?: {
accountToSign?: string;
},
) => Promise<XDR_BASE64>;
signAuthEntry?: SignAuthEntry;
/** The address of the contract the client will interact with. */
contractId: string;
/**
Expand Down Expand Up @@ -176,6 +218,24 @@ export type AssembledTransactionOptions<T = string> = MethodOptions &
method: string;
args?: any[];
parseResultXdr: (xdr: xdr.ScVal) => T;

/**
* The address of the account that should sign the transaction. Useful when
* a wallet holds multiple addresses to ensure signing with the intended one.
*/
address?: string;

/**
* This option will be passed through to the SEP43-compatible wallet extension. If true, and if the wallet supports it, the transaction will be signed and immediately submitted to the network by the wallet, bypassing the submit logic in {@link SentTransaction}.
* @default false
*/
submit?: boolean;

/**
* The URL of the network to which the transaction should be submitted.
* Only applicable when 'submit' is set to true.
*/
submitUrl?: string;
};

/**
Expand Down

0 comments on commit 40f2175

Please sign in to comment.