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

Fix/sign transaction type #1097

Merged
merged 15 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading