Skip to content

Commit

Permalink
[PSDK-361] Gasless Sends Support
Browse files Browse the repository at this point in the history
  • Loading branch information
John-peterson-coinbase committed Aug 12, 2024
1 parent d0b08ae commit fc8df8c
Show file tree
Hide file tree
Showing 13 changed files with 873 additions and 220 deletions.
4 changes: 2 additions & 2 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ module.exports = {
"./src/coinbase/**": {
branches: 80,
functions: 90,
statements: 95,
lines: 95,
statements: 90,
lines: 90,
},
},
};
155 changes: 101 additions & 54 deletions src/client/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,12 @@ export interface CreateTransferRequest {
* @memberof CreateTransferRequest
*/
'destination': string;
/**
* Whether the transfer uses sponsored gas
* @type {boolean}
* @memberof CreateTransferRequest
*/
'gasless'?: boolean;
}
/**
*
Expand Down Expand Up @@ -832,56 +838,6 @@ export interface ModelError {
*/
'message': string;
}
/**
* The native eth staking context.
* @export
* @interface NativeEthStakingContext
*/
export interface NativeEthStakingContext {
/**
*
* @type {Balance}
* @memberof NativeEthStakingContext
*/
'stakeable_balance': Balance;
/**
*
* @type {Balance}
* @memberof NativeEthStakingContext
*/
'unstakeable_balance': Balance;
/**
*
* @type {Balance}
* @memberof NativeEthStakingContext
*/
'claimable_balance': Balance;
}
/**
* The partial eth staking context.
* @export
* @interface PartialEthStakingContext
*/
export interface PartialEthStakingContext {
/**
*
* @type {Balance}
* @memberof PartialEthStakingContext
*/
'stakeable_balance': Balance;
/**
*
* @type {Balance}
* @memberof PartialEthStakingContext
*/
'unstakeable_balance': Balance;
/**
*
* @type {Balance}
* @memberof PartialEthStakingContext
*/
'claimable_balance': Balance;
}
/**
* An event representing a seed creation.
* @export
Expand Down Expand Up @@ -1171,6 +1127,66 @@ export interface SignedVoluntaryExitMessageMetadata {
*/
'signed_voluntary_exit': string;
}
/**
* An onchain sponsored gasless send.
* @export
* @interface SponsoredSend
*/
export interface SponsoredSend {
/**
* The onchain address of the recipient
* @type {string}
* @memberof SponsoredSend
*/
'to_address_id': string;
/**
* The raw typed data for the sponsored send
* @type {string}
* @memberof SponsoredSend
*/
'raw_typed_data': string;
/**
* The typed data hash for the sponsored send. This is the typed data hash that needs to be signed by the sender.
* @type {string}
* @memberof SponsoredSend
*/
'typed_data_hash': string;
/**
* The signed hash of the sponsored send typed data.
* @type {string}
* @memberof SponsoredSend
*/
'signature'?: string;
/**
* The hash of the onchain sponsored send transaction
* @type {string}
* @memberof SponsoredSend
*/
'transaction_hash'?: string;
/**
* The link to view the transaction on a block explorer. This is optional and may not be present for all transactions.
* @type {string}
* @memberof SponsoredSend
*/
'transaction_link'?: string;
/**
* The status of the sponsored send
* @type {string}
* @memberof SponsoredSend
*/
'status': SponsoredSendStatusEnum;
}

export const SponsoredSendStatusEnum = {
Pending: 'pending',
Signed: 'signed',
Submitted: 'submitted',
Complete: 'complete',
Failed: 'failed'
} as const;

export type SponsoredSendStatusEnum = typeof SponsoredSendStatusEnum[keyof typeof SponsoredSendStatusEnum];

/**
* Context needed to perform a staking operation
* @export
Expand All @@ -1185,11 +1201,30 @@ export interface StakingContext {
'context': StakingContextContext;
}
/**
* @type StakingContextContext
*
* @export
* @interface StakingContextContext
*/
export type StakingContextContext = NativeEthStakingContext | PartialEthStakingContext;

export interface StakingContextContext {
/**
*
* @type {Balance}
* @memberof StakingContextContext
*/
'stakeable_balance': Balance;
/**
*
* @type {Balance}
* @memberof StakingContextContext
*/
'unstakeable_balance': Balance;
/**
*
* @type {Balance}
* @memberof StakingContextContext
*/
'claimable_balance': Balance;
}
/**
* A list of onchain transactions to help realize a staking action.
* @export
Expand Down Expand Up @@ -1576,7 +1611,13 @@ export interface Transfer {
* @type {Transaction}
* @memberof Transfer
*/
'transaction': Transaction;
'transaction'?: Transaction;
/**
*
* @type {SponsoredSend}
* @memberof Transfer
*/
'sponsored_send'?: SponsoredSend;
/**
* The unsigned payload of the transfer. This is the payload that needs to be signed by the sender.
* @type {string}
Expand All @@ -1601,6 +1642,12 @@ export interface Transfer {
* @memberof Transfer
*/
'status'?: TransferStatusEnum;
/**
* Whether the transfer uses sponsored gas
* @type {boolean}
* @memberof Transfer
*/
'gasless': boolean;
}

export const TransferStatusEnum = {
Expand Down
20 changes: 5 additions & 15 deletions src/coinbase/address/wallet_address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ export class WalletAddress extends Address {
* @param options.destination - The destination of the transfer. If a Wallet, sends to the Wallet's default address. If a String, interprets it as the address ID.
* @param options.timeoutSeconds - The maximum amount of time to wait for the Transfer to complete, in seconds.
* @param options.intervalSeconds - The interval at which to poll the Network for Transfer status, in seconds.
* @param options.gasless - Whether the Transfer should be gasless. Defaults to false.
* @returns The transfer object.
* @throws {APIError} if the API request to create a Transfer fails.
* @throws {APIError} if the API request to broadcast a Transfer fails.
Expand All @@ -159,6 +160,7 @@ export class WalletAddress extends Address {
destination,
timeoutSeconds = 10,
intervalSeconds = 0.2,
gasless = false,
}: CreateTransferOptions): Promise<Transfer> {
if (!Coinbase.useServerSigner && !this.key) {
throw new InternalError("Cannot transfer from address without private key loaded");
Expand All @@ -180,6 +182,7 @@ export class WalletAddress extends Address {
network_id: destinationNetworkId,
asset_id: asset.primaryDenomination(),
destination: destinationAddress,
gasless: gasless,
};

let response = await Coinbase.apiClients.transfer!.createTransfer(
Expand All @@ -192,22 +195,9 @@ export class WalletAddress extends Address {

if (!Coinbase.useServerSigner) {
const wallet = new ethers.Wallet(this.key!.privateKey);
const transaction = transfer.getTransaction();
let signedPayload = await wallet!.signTransaction(transaction);
signedPayload = signedPayload.slice(2);

const broadcastTransferRequest = {
signed_payload: signedPayload,
};

response = await Coinbase.apiClients.transfer!.broadcastTransfer(
this.getWalletId(),
this.getId(),
transfer.getId(),
broadcastTransferRequest,
);
await transfer.sign(wallet);

transfer = Transfer.fromModel(response.data);
transfer = await transfer.broadcast();
}

const startTime = Date.now();
Expand Down
20 changes: 20 additions & 0 deletions src/coinbase/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,23 @@ export class InvalidUnsignedPayload extends Error {
}
}
}

/**
* AlreadySignedError is thrown when a resource is already signed.
*/
export class AlreadySignedError extends Error {
static DEFAULT_MESSAGE = "Resource already signed";

/**
* Initializes a new AlreadySignedError instance.
*
* @param message - The error message.
*/
constructor(message: string = AlreadySignedError.DEFAULT_MESSAGE) {
super(message);
this.name = "AlreadySignedError";
if (Error.captureStackTrace) {
Error.captureStackTrace(this, AlreadySignedError);
}
}
}
Loading

0 comments on commit fc8df8c

Please sign in to comment.