Skip to content

Commit

Permalink
Added new transaction type based on EIP-2930 (#1702)
Browse files Browse the repository at this point in the history
Signed-off-by: Antonio Mindov <[email protected]>
  • Loading branch information
rokn authored Jul 3, 2023
1 parent 395c6e0 commit f66dce0
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 4 deletions.
26 changes: 26 additions & 0 deletions src/Cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ class Cache {
/** @type {((bytes: Uint8Array) => EthereumTransactionData) | null} */
this._ethereumTransactionDataEip1559FromBytes = null;

/** @type {((bytes: Uint8Array) => EthereumTransactionData) | null} */
this._ethereumTransactionDataEip2930FromBytes = null;

/** @type {(() => TransactionReceiptQuery) | null} */
this._transactionReceiptQueryConstructor = null;

Expand Down Expand Up @@ -357,6 +360,29 @@ class Cache {
return this._ethereumTransactionDataEip1559FromBytes;
}

/**
* @param {((bytes: Uint8Array) => EthereumTransactionData)} ethereumTransactionDataEip2930FromBytes
*/
setEthereumTransactionDataEip2930FromBytes(
ethereumTransactionDataEip2930FromBytes
) {
this._ethereumTransactionDataEip2930FromBytes =
ethereumTransactionDataEip2930FromBytes;
}

/**
* @returns {((bytes: Uint8Array) => EthereumTransactionData)}
*/
get ethereumTransactionDataEip2930FromBytes() {
if (this._ethereumTransactionDataEip2930FromBytes == null) {
throw new Error(
"Cache.ethereumTransactionDataEip2930FromBytes was used before it was set"
);
}

return this._ethereumTransactionDataEip2930FromBytes;
}

/**
* @param {(() => TransactionReceiptQuery)} transactionReceiptQueryConstructor
*/
Expand Down
11 changes: 7 additions & 4 deletions src/EthereumTransactionData.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,13 @@ export default class EthereumTransactionData {
throw new Error("empty bytes");
}

if (bytes[0] != 2) {
return CACHE.ethereumTransactionDataLegacyFromBytes(bytes);
} else {
return CACHE.ethereumTransactionDataEip1559FromBytes(bytes);
switch (bytes[0]) {
case 1:
return CACHE.ethereumTransactionDataEip2930FromBytes(bytes);
case 2:
return CACHE.ethereumTransactionDataEip1559FromBytes(bytes);
default:
return CACHE.ethereumTransactionDataLegacyFromBytes(bytes);
}
}

Expand Down
140 changes: 140 additions & 0 deletions src/EthereumTransactionDataEip2930.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import * as rlp from "@ethersproject/rlp";
import * as hex from "./encoding/hex.js";
import EthereumTransactionData from "./EthereumTransactionData.js";
import CACHE from "./Cache.js";

/**
* @typedef {object} EthereumTransactionDataEip2930JSON
* @property {string} chainId
* @property {string} nonce
* @property {string} gasPrice
* @property {string} gasLimit
* @property {string} to
* @property {string} value
* @property {string} callData
* @property {string[]} accessList
* @property {string} recId
* @property {string} r
* @property {string} s
*/

export default class EthereumTransactionDataEip2930 extends EthereumTransactionData {
/**
* @private
* @param {object} props
* @param {Uint8Array} props.chainId
* @param {Uint8Array} props.nonce
* @param {Uint8Array} props.gasPrice
* @param {Uint8Array} props.gasLimit
* @param {Uint8Array} props.to
* @param {Uint8Array} props.value
* @param {Uint8Array} props.callData
* @param {Uint8Array[]} props.accessList
* @param {Uint8Array} props.recId
* @param {Uint8Array} props.r
* @param {Uint8Array} props.s
*/
constructor(props) {
super(props);

this.chainId = props.chainId;
this.nonce = props.nonce;
this.gasPrice = props.gasPrice;
this.gasLimit = props.gasLimit;
this.to = props.to;
this.value = props.value;
this.accessList = props.accessList;
this.recId = props.recId;
this.r = props.r;
this.s = props.s;
}

/**
* @param {Uint8Array} bytes
* @returns {EthereumTransactionData}
*/
static fromBytes(bytes) {
if (bytes.length === 0) {
throw new Error("empty bytes");
}

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const decoded = /** @type {string[]} */ (rlp.decode(bytes.subarray(1)));

if (!Array.isArray(decoded)) {
throw new Error("ethereum data is not a list");
}

if (decoded.length !== 11) {
throw new Error("invalid ethereum transaction data");
}

// TODO
return new EthereumTransactionDataEip2930({
chainId: hex.decode(/** @type {string} */ (decoded[0])),
nonce: hex.decode(/** @type {string} */ (decoded[1])),
gasPrice: hex.decode(/** @type {string} */ (decoded[2])),
gasLimit: hex.decode(/** @type {string} */ (decoded[3])),
to: hex.decode(/** @type {string} */ (decoded[4])),
value: hex.decode(/** @type {string} */ (decoded[5])),
callData: hex.decode(/** @type {string} */ (decoded[6])),
// @ts-ignore
accessList: /** @type {string[]} */ (decoded[7]).map((v) =>
hex.decode(v)
),
recId: hex.decode(/** @type {string} */ (decoded[8])),
r: hex.decode(/** @type {string} */ (decoded[9])),
s: hex.decode(/** @type {string} */ (decoded[10])),
});
}

/**
* @returns {Uint8Array}
*/
toBytes() {
const encoded = rlp.encode([
this.chainId,
this.nonce,
this.gasPrice,
this.gasLimit,
this.to,
this.value,
this.callData,
this.accessList,
this.recId,
this.r,
this.s,
]);
return hex.decode("01" + encoded.substring(2));
}

/**
* @returns {string}
*/
toString() {
return JSON.stringify(this.toJSON(), null, 2);
}

/**
* @returns {EthereumTransactionDataEip2930JSON}
*/
toJSON() {
return {
chainId: hex.encode(this.chainId),
nonce: hex.encode(this.nonce),
gasPrice: hex.encode(this.gasPrice),
gasLimit: hex.encode(this.gasLimit),
to: hex.encode(this.to),
value: hex.encode(this.value),
callData: hex.encode(this.callData),
accessList: this.accessList.map((v) => hex.encode(v)),
recId: hex.encode(this.recId),
r: hex.encode(this.r),
s: hex.encode(this.s),
};
}
}

CACHE.setEthereumTransactionDataEip2930FromBytes((bytes) =>
EthereumTransactionDataEip2930.fromBytes(bytes)
);
1 change: 1 addition & 0 deletions src/exports.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export { default as DelegateContractId } from "./contract/DelegateContractId.js"
export { default as EthereumTransaction } from "./EthereumTransaction.js";
export { default as EthereumTransactionDataLegacy } from "./EthereumTransactionDataLegacy.js";
export { default as EthereumTransactionDataEip1559 } from "./EthereumTransactionDataEip1559.js";
export { default as EthereumTransactionDataEip2930 } from "./EthereumTransactionDataEip2930.js";
export { default as EthereumTransactionData } from "./EthereumTransactionData.js";
export { default as EthereumFlow } from "./EthereumFlow.js";
export { default as EvmAddress } from "./EvmAddress.js";
Expand Down
7 changes: 7 additions & 0 deletions test/unit/EthereumTransactionData.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ const rawTxType0 = hex.decode(
"f864012f83018000947e3a9eaf9bcc39e2ffa38eb30bf7a93feacbc18180827653820277a0f9fbff985d374be4a55f296915002eec11ac96f1ce2df183adf992baa9390b2fa00c1e867cc960d9c74ec2e6a662b7908ec4c8cc9f3091e886bcefbeb2290fb792"
);

const rawTxType1 = hex.decode(
"01f85f01010a0a9400000000000000000000000000000000000000010a80c080a038ba8bdbcd8684ff089b8efaf7b5aaf2071a11ab01b6cc65757af79f1199f2efa0570b83f85d578427becab466ced52da857e2a9e48bf9ec5850cc2f541e9305e9"
);

// These byte fail to be decoded by @ethersproject/rlp
// const rawTxType0TrimmedLastBytes =
// hex.decode("f864012f83018000947e3a9eaf9bcc39e2ffa38eb30bf7a93feacbc18180827653820277a0f9fbff985d374be4a55f296915002eec11ac96f1ce2df183adf992baa9390b2fa00c1e867cc960d9c74ec2e6a662b7908ec4c8cc9f3091e886bcefbeb2290000");
Expand All @@ -20,6 +24,9 @@ describe("EthereumTransactionData", function () {
expect(
EthereumTransactionData.fromBytes(rawTxType0).toBytes()
).to.deep.equal(rawTxType0);
expect(
EthereumTransactionData.fromBytes(rawTxType1).toBytes()
).to.deep.equal(rawTxType1);
expect(
EthereumTransactionData.fromBytes(rawTxType2).toBytes()
).to.deep.equal(rawTxType2);
Expand Down

0 comments on commit f66dce0

Please sign in to comment.