From ee982667631fdf8898f6728a7481412b222141bc Mon Sep 17 00:00:00 2001 From: John Peterson Date: Tue, 14 May 2024 12:09:05 -0700 Subject: [PATCH] [PSDK-117] Implement Transfer Class --- src/coinbase/coinbase.ts | 5 ++ src/coinbase/errors.ts | 19 +++++ src/coinbase/transfer.ts | 160 +++++++++++++++++++++++++++++++++++++++ src/coinbase/user.ts | 2 +- 4 files changed, 185 insertions(+), 1 deletion(-) create mode 100644 src/coinbase/transfer.ts diff --git a/src/coinbase/coinbase.ts b/src/coinbase/coinbase.ts index 6d3f28e9..8a45352a 100644 --- a/src/coinbase/coinbase.ts +++ b/src/coinbase/coinbase.ts @@ -13,6 +13,11 @@ import { InvalidAPIKeyFormat, InternalError, InvalidConfiguration } from "./erro export class Coinbase { apiClients: ApiClients = {}; + /** + * Represents the number of Wei per Ether. + */ + static readonly WEI_PER_ETHER: bigint = BigInt("1000000000000000000"); + /** * Initializes the Coinbase SDK. * @constructor diff --git a/src/coinbase/errors.ts b/src/coinbase/errors.ts index 94d50f0a..796a48f6 100644 --- a/src/coinbase/errors.ts +++ b/src/coinbase/errors.ts @@ -55,3 +55,22 @@ export class InvalidConfiguration extends Error { } } } + +/** + * InvalidUnsignedPayload error is thrown when the unsigned payload is invalid. + */ +export class InvalidUnsignedPayload extends Error { + static DEFAULT_MESSAGE = "Invalid unsigned payload"; + + /** + * Initializes a new InvalidUnsignedPayload instance. + * @param message - The error message. + */ + constructor(message: string = InvalidUnsignedPayload.DEFAULT_MESSAGE) { + super(message); + this.name = "InvalidUnsignedPayload"; + if (Error.captureStackTrace) { + Error.captureStackTrace(this, InvalidUnsignedPayload); + } + } +} diff --git a/src/coinbase/transfer.ts b/src/coinbase/transfer.ts new file mode 100644 index 00000000..6e8cb27b --- /dev/null +++ b/src/coinbase/transfer.ts @@ -0,0 +1,160 @@ +import { ApiClients } from "./types"; +import { Coinbase } from "./coinbase"; +import { Transfer as TransferModel } from "../client/api"; +import { ethers } from "ethers"; +import { InvalidUnsignedPayload } from "./errors"; + +/** + * A representation of a Transfer, which moves an amount of an Asset from + * a user-controlled Wallet to another Address. The fee is assumed to be paid + * in the native Asset of the Network. + */ +export class Transfer { + private model: TransferModel; + private client: ApiClients; + private transaction?: ethers.Transaction; + + /** + * Initializes a new Transfer instance. + * @param {TransferModel} transferModel - The Transfer model. + * @param {ApiClients} client - The API clients. + */ + constructor(transferModel: TransferModel, client: ApiClients) { + this.model = transferModel; + this.client = client; + } + + /** + * Returns the ID of the Transfer. + * @returns {string} The Transfer ID. + */ + public getId(): string { + return this.model.transfer_id; + } + + /** + * Returns the Network ID of the Transfer. + * @returns {string} The Network ID. + */ + public getNetworkId(): string { + return this.model.network_id; + } + + /** + * Returns the Wallet ID of the Transfer. + * @returns {string} The Wallet ID. + */ + public getWalletId(): string { + return this.model.wallet_id; + } + + /** + * Returns the From Address ID of the Transfer. + * @returns {string} The From Address ID. + */ + public getFromAddressId(): string { + return this.model.address_id; + } + + /** + * Returns the Destination Address ID of the Transfer. + * @returns {string} The Destination Address ID. + */ + public getDestinationAddressId(): string { + return this.model.destination; + } + + /** + * Returns the Asset ID of the Transfer. + * @returns {string} The Asset ID. + */ + public getAssetId(): string { + return this.model.asset_id; + } + + /** + * Returns the Amount of the Transfer. + * @returns {string} The Amount of the Asset. + */ + public getAmount(): bigint { + const amount = BigInt(this.model.amount); + + if (this.getAssetId() === "ETH") { + return amount / Coinbase.WEI_PER_ETHER; + } + return BigInt(this.model.amount); + } + + /** + * Returns the Unsigned Payload of the Transfer. + * @returns {string} The Unsigned Payload as a Hex string. + */ + public getUnsignedPayload(): string { + return this.model.unsigned_payload; + } + + /** + * Returns the Signed Payload of the Transfer. + * @returns {string | undefined} The Signed Payload as a Hex string, or undefined if not yet available. + */ + public getSignedPayload(): string | undefined { + return this.model.signed_payload; + } + + /** + * Returns the Transaction Hash of the Transfer. + * @returns {string | undefined} The Transaction Hash as a Hex string, or undefined if not yet available + */ + public getTransactionhash(): string | undefined { + return this.model.transaction_hash; + } + + /** + * Returns the Transaction of the Transfer. + * @returns {ethers.Transaction} The ethers.js Transaction object. + * @throws (InvalidUnsignedPayload) If the unsigned payload is invalid. + */ + public getTransaction(): ethers.Transaction { + if (this.transaction) return this.transaction; + + const transaction = new ethers.Transaction(); + + const rawPayload = this.getUnsignedPayload() + .match(/../g) + ?.map(byte => parseInt(byte, 16)); + if (!rawPayload) { + throw new InvalidUnsignedPayload("Unable to parse unsigned payload"); + } + + const rawPayloadBytes = new Uint8Array(rawPayload); + + const decoder = new TextDecoder(); + + let parsedPayload; + try { + parsedPayload = JSON.parse(decoder.decode(rawPayloadBytes)); + } catch (error) { + throw new InvalidUnsignedPayload("Unable to decode unsigned payload JSON"); + } + + transaction.chainId = BigInt(parsedPayload["chainId"]); + transaction.nonce = BigInt(parsedPayload["nonce"]); + transaction.maxPriorityFeePerGas = BigInt(parsedPayload["maxPriorityFeePerGas"]); + transaction.maxFeePerGas = BigInt(parsedPayload["maxFeePerGas"]); + transaction.gasLimit = BigInt(parsedPayload["gas"]); + transaction.to = parsedPayload["to"]; + transaction.value = BigInt(parsedPayload["value"]); + transaction.data = parsedPayload["input"]; + + this.transaction = transaction; + return transaction; + } + + /** + * Sets the signed Transaction of the Transfer. + * @param {ethers.Transaction} transaction - The signed Transaction. + */ + public setSignedTransaction(transaction: ethers.Transaction): void { + this.transaction = transaction; + } +} diff --git a/src/coinbase/user.ts b/src/coinbase/user.ts index b7f144e4..16b008b6 100644 --- a/src/coinbase/user.ts +++ b/src/coinbase/user.ts @@ -12,7 +12,7 @@ export class User { /** * Initializes a new User instance. - * @param {UserModel} user - The user model. + * @param {UserModel} user - The User model. * @param {ApiClients} client - The API clients. */ constructor(user: UserModel, client: ApiClients) {