From 6b82f31c061ef89cd8c8327982eba554e0c08867 Mon Sep 17 00:00:00 2001 From: Ryan Ghods Date: Thu, 9 May 2024 11:01:46 -0700 Subject: [PATCH] add offererSignature and a way to derive it from the connected ethers signer (#1461) --- package.json | 2 +- src/api/api.ts | 8 ++- src/sdk.ts | 84 +++++++++++++++++++++++++++--- src/types.ts | 2 +- src/utils/utils.ts | 49 +++++++++++++++++ test/integration/postOrder.spec.ts | 19 ++++++- 6 files changed, 152 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 12f62340b..d6d96825e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "opensea-js", - "version": "7.1.8", + "version": "7.1.9", "description": "TypeScript SDK for the OpenSea marketplace helps developers build new experiences using NFTs and our marketplace data", "license": "MIT", "author": "OpenSea Developers", diff --git a/src/api/api.ts b/src/api/api.ts index 718083e57..b40bf46d0 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -639,16 +639,22 @@ export class OpenSeaAPI { * @param protocolAddress The Seaport address for the order. * @param orderHash The order hash, or external identifier, of the order. * @param chain The chain where the order is located. + * @param offererSignature An EIP-712 signature from the offerer of the order. + * If this is not provided, the user associated with the API Key will be checked instead. + * The signature must be a EIP-712 signature consisting of the order's Seaport contract's + * name, version, address, and chain. The struct to sign is `OrderHash` containing a + * single bytes32 field. * @returns The response from the API. */ public async offchainCancelOrder( protocolAddress: string, orderHash: string, chain: Chain = this.chain, + offererSignature?: string, ): Promise { const response = await this.post( getCancelOrderPath(chain, protocolAddress, orderHash), - {}, + { offererSignature }, ); return response; } diff --git a/src/sdk.ts b/src/sdk.ts index 3551576a0..3261a33ef 100644 --- a/src/sdk.ts +++ b/src/sdk.ts @@ -63,6 +63,7 @@ import { isTestChain, basisPointsForFee, totalBasisPointsForFees, + getChainId, } from "./utils/utils"; /** @@ -916,21 +917,81 @@ export class OpenSeaSDK { ); } + private _getSeaportVersion(protocolAddress: string) { + const protocolAddressChecksummed = ethers.getAddress(protocolAddress); + switch (protocolAddressChecksummed) { + case CROSS_CHAIN_SEAPORT_V1_6_ADDRESS: + return "1.6"; + case CROSS_CHAIN_SEAPORT_V1_5_ADDRESS: + return "1.5"; + default: + throw new Error("Unknown or unsupported protocol address"); + } + } + + /** + * Get the offerer signature for canceling an order offchain. + * The signature will only be valid if the signer address is the address of the order's offerer. + */ + private async _getOffererSignature( + protocolAddress: string, + orderHash: string, + chain: Chain, + ) { + const chainId = getChainId(chain); + const name = "Seaport"; + const version = this._getSeaportVersion(protocolAddress); + + if ( + typeof (this._signerOrProvider as Signer).signTypedData == "undefined" + ) { + throw new Error( + "Please pass an ethers Signer into this sdk to derive an offerer signature", + ); + } + + return (this._signerOrProvider as Signer).signTypedData( + { chainId, name, version, verifyingContract: protocolAddress }, + { OrderHash: [{ name: "orderHash", type: "bytes32" }] }, + { orderHash }, + ); + } + /** * Offchain cancel an order, offer or listing, by its order hash when protected by the SignedZone. * Protocol and Chain are required to prevent hash collisions. * Please note cancellation is only assured if a fulfillment signature was not vended prior to cancellation. * @param protocolAddress The Seaport address for the order. - * @param orderJash The order hash, or external identifier, of the order. + * @param orderHash The order hash, or external identifier, of the order. * @param chain The chain where the order is located. + * @param offererSignature An EIP-712 signature from the offerer of the order. + * If this is not provided, the user associated with the API Key will be checked instead. + * The signature must be a EIP-712 signature consisting of the order's Seaport contract's + * name, version, address, and chain. The struct to sign is `OrderHash` containing a + * single bytes32 field. + * @param useSignerToDeriveOffererSignature Derive the offererSignature from the Ethers signer passed into this sdk. * @returns The response from the API. */ public async offchainCancelOrder( protocolAddress: string, orderHash: string, chain: Chain = this.chain, + offererSignature?: string, + useSignerToDeriveOffererSignature?: boolean, ) { - return this.api.offchainCancelOrder(protocolAddress, orderHash, chain); + if (useSignerToDeriveOffererSignature) { + offererSignature = await this._getOffererSignature( + protocolAddress, + orderHash, + chain, + ); + } + return this.api.offchainCancelOrder( + protocolAddress, + orderHash, + chain, + offererSignature, + ); } /** @@ -1220,12 +1281,8 @@ export class OpenSeaSDK { this._emitter.emit(event, data); } - /** - * Throws an error if an account is not available through the provider. - * @param accountAddress The account address to check is available. - */ - private async _requireAccountIsAvailable(accountAddress: string) { - const accountAddressChecksummed = ethers.getAddress(accountAddress); + /** Get the accounts available from the signer or provider. */ + private async _getAvailableAccounts() { const availableAccounts: string[] = []; if ("address" in this._signerOrProvider) { @@ -1237,6 +1294,17 @@ export class OpenSeaSDK { availableAccounts.push(...addresses); } + return availableAccounts; + } + + /** + * Throws an error if an account is not available through the provider. + * @param accountAddress The account address to check is available. + */ + private async _requireAccountIsAvailable(accountAddress: string) { + const accountAddressChecksummed = ethers.getAddress(accountAddress); + const availableAccounts = await this._getAvailableAccounts(); + if (availableAccounts.includes(accountAddressChecksummed)) { return; } diff --git a/src/types.ts b/src/types.ts index 8349a6d6d..40caab88f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -95,7 +95,7 @@ export interface OpenSeaAPIConfig { /** * Each of the possible chains that OpenSea supports. - * ⚠️NOTE: When adding to this list, also add to the util function `getWETHAddress` + * ⚠️NOTE: When adding to this list, also add to the util functions `getChainId` and `getWETHAddress` */ export enum Chain { // Mainnet Chains diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 13fca933a..d3111ea13 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -168,6 +168,55 @@ export const getAssetItemType = (tokenStandard: TokenStandard) => { } }; +export const getChainId = (chain: Chain) => { + switch (chain) { + case Chain.Mainnet: + return "1"; + case Chain.Polygon: + return "137"; + case Chain.Amoy: + return "80002"; + case Chain.Sepolia: + return "11155111"; + case Chain.Klaytn: + return "8217"; + case Chain.Baobab: + return "1001"; + case Chain.Avalanche: + return "43114"; + case Chain.Fuji: + return "43113"; + case Chain.BNB: + return "56"; + case Chain.BNBTestnet: + return "97"; + case Chain.Arbitrum: + return "42161"; + case Chain.ArbitrumNova: + return "42170"; + case Chain.ArbitrumSepolia: + return "421614"; + case Chain.Blast: + return "238"; + case Chain.BlastSepolia: + return "168587773"; + case Chain.Base: + return "8453"; + case Chain.BaseSepolia: + return "84532"; + case Chain.Optimism: + return "10"; + case Chain.OptimismSepolia: + return "11155420"; + case Chain.Zora: + return "7777777"; + case Chain.ZoraSepolia: + return "999999999"; + default: + throw new Error(`Unknown chainId for ${chain}`); + } +}; + export const getWETHAddress = (chain: Chain) => { switch (chain) { case Chain.Mainnet: diff --git a/test/integration/postOrder.spec.ts b/test/integration/postOrder.spec.ts index 29789609c..3a67f6534 100644 --- a/test/integration/postOrder.spec.ts +++ b/test/integration/postOrder.spec.ts @@ -114,9 +114,11 @@ suite("SDK: order posting", () => { paymentTokenAddress, }; const offerResponse = await sdk.createCollectionOffer(postOrderRequest); + expect(offerResponse).to.exist.and.to.have.property("protocol_address"); expect(offerResponse).to.exist.and.to.have.property("protocol_data"); + expect(offerResponse).to.exist.and.to.have.property("order_hash"); - // Cancel the order + // Cancel the order using self serve API key tied to the offerer const { protocol_address, order_hash } = offerResponse!; const cancelResponse = await sdk.offchainCancelOrder( protocol_address, @@ -139,7 +141,22 @@ suite("SDK: order posting", () => { }; const offerResponse = await sdkPolygon.createCollectionOffer(postOrderRequest); + expect(offerResponse).to.exist.and.to.have.property("protocol_address"); expect(offerResponse).to.exist.and.to.have.property("protocol_data"); + expect(offerResponse).to.exist.and.to.have.property("order_hash"); + + // Cancel the order using the offerer signature, deriving it from the ethers signer + const { protocol_address, order_hash } = offerResponse!; + const cancelResponse = await sdkPolygon.offchainCancelOrder( + protocol_address, + order_hash, + undefined, + undefined, + true, + ); + expect(cancelResponse).to.exist.and.to.have.property( + "last_signature_issued_valid_until", + ); }); test("Post Trait Offer - Ethereum", async () => {