Skip to content

Add support for executeMetaTransaction #1974

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

Merged
merged 4 commits into from
Nov 20, 2023
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 .changeset/olive-readers-marry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@thirdweb-dev/sdk": patch
---

Add support for new relayer and polygon relayer
208 changes: 206 additions & 2 deletions packages/sdk/src/evm/core/classes/contract-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,8 @@ export class ContractWrapper<
if (
this.options?.gasless &&
("openzeppelin" in this.options.gasless ||
"biconomy" in this.options.gasless)
"biconomy" in this.options.gasless ||
"engine" in this.options.gasless)
) {
if (fn === "multicall" && Array.isArray(args[0]) && args[0].length > 0) {
const from = await this.getSignerAddress();
Expand Down Expand Up @@ -632,8 +633,211 @@ export class ContractWrapper<
): Promise<string> {
if (this.options.gasless && "biconomy" in this.options.gasless) {
return this.biconomySendFunction(transaction);
} else if (this.options.gasless && "openzeppelin" in this.options.gasless) {
return this.defenderSendFunction(transaction);
}
return this.engineSendFunction(transaction);
}

private async engineSendFunction(
transaction: GaslessTransaction,
): Promise<string> {
invariant(
this.options.gasless && "engine" in this.options.gasless,
"calling engine gasless transaction without engine config in the SDK options",
);

const request = await this.enginePrepareRequest(transaction);

const res = await fetch(this.options.gasless.engine.relayerUrl, {
...request,
headers: {
"Content-Type": "application/json",
},
});
const data = await res.json();

if (data.error) {
throw new Error(data.error?.message || JSON.stringify(data.error));
}

const queueId = data.result.queueId as string;
const engineUrl =
this.options.gasless.engine.relayerUrl.split("/relayer/")[0];
const startTime = Date.now();
while (true) {
const txRes = await fetch(`${engineUrl}/transaction/status/${queueId}`);
const txData = await txRes.json();

if (txData.result.transactionHash) {
return txData.result.transactionHash as string;
}

// Time out after 30s
if (Date.now() - startTime > 30 * 1000) {
throw new Error("timeout");
}

// Poll to check if the transaction was mined
await new Promise((resolve) => setTimeout(resolve, 2000));
}
}

private async enginePrepareRequest(transaction: GaslessTransaction) {
const signer = this.getSigner();
const provider = this.getProvider();
const storage = this.storage;

invariant(signer, "signer is not set");

try {
const { abi } = await fetchContractMetadataFromAddress(
transaction.to,
provider,
storage,
);

const chainId = (await provider.getNetwork()).chainId;
const contract = new ContractWrapper(
signer,
transaction.to,
abi,
{},
storage,
);
if (abi.find((item) => item.name === "executeMetaTransaction")) {
const name: string = await contract.call("name", []);

const domain = {
name,
version: "1",
salt: "0x" + chainId.toString(16).padStart(64, "0"), // Use 64 length hex chain id as salt
verifyingContract: transaction.to,
};

const types = {
MetaTransaction: [
{ name: "nonce", type: "uint256" },
{ name: "from", type: "address" },
{ name: "functionSignature", type: "bytes" },
],
};

const nonce = await contract.call("getNonce", [transaction.from]);
const message = {
nonce: nonce,
from: transaction.from,
functionSignature: transaction.data,
};

const { signature } = await signTypedDataInternal(
signer,
domain,
types,
message,
);

return {
method: "POST",
body: JSON.stringify({
type: "execute-meta-transaction",
request: {
from: transaction.from,
to: transaction.to,
data: transaction.data,
},
signature,
}),
};
}
} catch {
// no-op
}

if (
transaction.functionName === "approve" &&
transaction.functionArgs.length === 2
) {
const spender = transaction.functionArgs[0];
const amount = transaction.functionArgs[1];
// TODO: support DAI permit by signDAIPermit
const { message: permit, signature: sig } = await signEIP2612Permit(
signer,
transaction.to,
transaction.from,
spender,
amount,
);

const message = {
to: transaction.to,
owner: permit.owner,
spender: permit.spender,
value: BigNumber.from(permit.value).toString(),
nonce: BigNumber.from(permit.nonce).toString(),
deadline: BigNumber.from(permit.deadline).toString(),
};

return {
method: "POST",
body: JSON.stringify({
type: "permit",
request: message,
signature: sig,
}),
};
} else {
const forwarderAddress =
CONTRACT_ADDRESSES[
transaction.chainId as keyof typeof CONTRACT_ADDRESSES
].openzeppelinForwarder ||
(await computeForwarderAddress(provider, storage));
const ForwarderABI = (
await import("@thirdweb-dev/contracts-js/dist/abis/Forwarder.json")
).default;

const forwarder = new Contract(forwarderAddress, ForwarderABI, provider);
const nonce = await getAndIncrementNonce(forwarder, "getNonce", [
transaction.from,
]);

const domain = {
name: "GSNv2 Forwarder",
version: "0.0.1",
chainId: transaction.chainId,
verifyingContract: forwarderAddress,
};
const types = {
ForwardRequest,
};

const message = {
from: transaction.from,
to: transaction.to,
value: BigNumber.from(0).toString(),
gas: BigNumber.from(transaction.gasLimit).toString(),
nonce: BigNumber.from(nonce).toString(),
data: transaction.data,
};

const { signature: sig } = await signTypedDataInternal(
signer,
domain,
types,
message,
);
const signature: BytesLike = sig;

return {
method: "POST",
body: JSON.stringify({
type: "forward",
request: message,
signature,
forwarderAddress,
}),
};
}
return this.defenderSendFunction(transaction);
}

private async biconomySendFunction(
Expand Down
124 changes: 96 additions & 28 deletions packages/sdk/src/evm/core/classes/transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ import { signTypedDataInternal } from "../../common/sign";
import { BytesLike } from "ethers";
import { CONTRACT_ADDRESSES } from "../../constants/addresses/CONTRACT_ADDRESSES";
import { getContractAddressByChainId } from "../../constants/addresses/getContractAddressByChainId";
import { getCompositeABI } from "../../common/plugin/getCompositePluginABI";
import { ContractWrapper } from "./contract-wrapper";

abstract class TransactionContext {
protected args: any[];
Expand Down Expand Up @@ -1019,29 +1021,76 @@ async function enginePrepareRequest(
provider: providers.Provider,
storage: ThirdwebStorage,
) {
const forwarderAddress =
CONTRACT_ADDRESSES[transaction.chainId as keyof typeof CONTRACT_ADDRESSES]
.openzeppelinForwarder ||
(await computeForwarderAddress(provider, storage));
const ForwarderABI = (
await import("@thirdweb-dev/contracts-js/dist/abis/Forwarder.json")
).default;
try {
const metadata = await fetchContractMetadataFromAddress(
transaction.to,
provider,
storage,
);

const forwarder = new Contract(forwarderAddress, ForwarderABI, provider);
const nonce = await getAndIncrementNonce(forwarder, "getNonce", [
transaction.from,
]);
const chainId = (await provider.getNetwork()).chainId;
const abi = await getCompositeABI(
transaction.to,
metadata.abi,
provider,
{},
storage,
);
const contract = new ContractWrapper(
signer,
transaction.to,
abi,
{},
storage,
);
if (abi.find((item) => item.name === "executeMetaTransaction")) {
const name: string = await contract.call("name", []);

const domain = {
name,
version: "1",
salt: "0x" + chainId.toString(16).padStart(64, "0"), // Use 64 length hex chain id as salt
verifyingContract: transaction.to,
};

const types = {
MetaTransaction: [
{ name: "nonce", type: "uint256" },
{ name: "from", type: "address" },
{ name: "functionSignature", type: "bytes" },
],
};

const nonce = await contract.call("getNonce", [transaction.from]);
const message = {
nonce: nonce,
from: transaction.from,
functionSignature: transaction.data,
};

const { signature } = await signTypedDataInternal(
signer,
domain,
types,
message,
);

const domain = {
name: "GSNv2 Forwarder",
version: "0.0.1",
chainId: transaction.chainId,
verifyingContract: forwarderAddress,
};
const types = {
ForwardRequest,
};
let message: ForwardRequestMessage | PermitRequestMessage;
return {
method: "POST",
body: JSON.stringify({
type: "execute-meta-transaction",
request: {
from: transaction.from,
to: transaction.to,
data: transaction.data,
},
signature,
}),
};
}
} catch {
// no-op
}

if (
transaction.functionName === "approve" &&
Expand All @@ -1058,29 +1107,48 @@ async function enginePrepareRequest(
amount,
);

const { r, s, v } = utils.splitSignature(sig);

message = {
const message = {
to: transaction.to,
owner: permit.owner,
spender: permit.spender,
value: BigNumber.from(permit.value).toString(),
nonce: BigNumber.from(permit.nonce).toString(),
deadline: BigNumber.from(permit.deadline).toString(),
r,
s,
v,
};

return {
method: "POST",
body: JSON.stringify({
type: "permit",
request: message,
signature: sig,
}),
};
} else {
message = {
const forwarderAddress =
CONTRACT_ADDRESSES[transaction.chainId as keyof typeof CONTRACT_ADDRESSES]
.openzeppelinForwarder ||
(await computeForwarderAddress(provider, storage));
const ForwarderABI = (
await import("@thirdweb-dev/contracts-js/dist/abis/Forwarder.json")
).default;

const forwarder = new Contract(forwarderAddress, ForwarderABI, provider);
const nonce = await getAndIncrementNonce(forwarder, "getNonce", [
transaction.from,
]);

const domain = {
name: "GSNv2 Forwarder",
version: "0.0.1",
chainId: transaction.chainId,
verifyingContract: forwarderAddress,
};
const types = {
ForwardRequest,
};

const message = {
from: transaction.from,
to: transaction.to,
value: BigNumber.from(0).toString(),
Expand Down