@pushchain/core 6.0.21 → 6.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ @pushchain/core@6.0.22 (2026-08-14)
2
+
3
+ - fix: make PC20 first exports and Solana R3 E2E reliable
4
+ - fix: support PC20 exports for standard ERC-20 tokens
5
+ - feat: curated E2E CI suite with pre-flight fund gate
6
+
7
+ ---
8
+
1
9
  @pushchain/core@6.0.21 (2026-08-01)
2
10
 
3
11
  - fix: pc20 bug fix
@@ -11,8 +19,15 @@
11
19
  @pushchain/core@6.1.0 (unreleased)
12
20
 
13
21
  - Added first-class PC20 support to the universal transaction API. PC20 tokens
14
- are resolved from their chain and address through UniversalCore, without an
15
- explicit token-standard flag.
22
+ are identified by chain and address without an explicit token-standard flag.
23
+ External wrappers are resolved through UniversalCore, while Push-native
24
+ tokens are validated directly on Push Chain.
25
+
26
+ - Removed the custom `pc20Metadata()` requirement for Push-native PC20 exports.
27
+ Any metadata-compatible ERC-20 born on Push Chain can now be exported: the SDK
28
+ reads the standard `name()`, `symbol()`, and `decimals()` functions, verifies
29
+ deployed bytecode, and continues to reject synthetic PRC20 addresses from the
30
+ PC20 path.
16
31
 
17
32
  - Added `PushChain.utils.tokens.getPC20Address()` for resolving either a
18
33
  Push-native PC20 or an external wrapper and listing its confirmed deployments.
@@ -40,6 +55,10 @@
40
55
  account as the gateway recipient instead of the zero address, preventing the
41
56
  forwarded funds leg from attempting to credit `address(0)`.
42
57
 
58
+ - Fixed first PC20 wrapper deployments running out of gas on the destination.
59
+ The SDK now re-quotes UGPC with the resolved deployment-aware limit and writes
60
+ the corresponding explicit limit into the outbound request.
61
+
43
62
  - Realigned inbound protobuf fields with the chain, exposed PC20 inbound
44
63
  metadata, and added SVM remaining-account support required by the Solana
45
64
  gateway.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pushchain/core",
3
- "version": "6.0.21",
3
+ "version": "6.0.22",
4
4
  "homepage": "https://push.org",
5
5
  "keywords": [
6
6
  "pushchain",
@@ -50,7 +50,11 @@
50
50
  }
51
51
  },
52
52
  "scripts": {
53
- "build:proto": "scripts/protoc-generate.sh"
53
+ "build:proto": "scripts/protoc-generate.sh",
54
+ "e2e:list": "ts-node --transpile-only __e2e__/ci/run.ts --list",
55
+ "e2e:verify": "ts-node --transpile-only __e2e__/ci/run.ts --verify",
56
+ "e2e:preflight": "ts-node --transpile-only __e2e__/ci/preflight.ts",
57
+ "e2e:ci": "ts-node --transpile-only __e2e__/ci/run.ts"
54
58
  },
55
59
  "type": "commonjs",
56
60
  "main": "./src/index.js",
@@ -67,7 +71,9 @@
67
71
  ],
68
72
  "devDependencies": {
69
73
  "@types/bs58": "^4.0.0",
74
+ "dotenv": "^16.4.5",
70
75
  "secp256k1": "^5.0.1",
76
+ "ts-node": "10.9.1",
71
77
  "ts-proto": "^2.0.3"
72
78
  }
73
79
  }
@@ -1,4 +1,34 @@
1
1
  export declare const ERC20_EVM: readonly [{
2
+ readonly type: "function";
3
+ readonly name: "name";
4
+ readonly inputs: readonly [];
5
+ readonly outputs: readonly [{
6
+ readonly name: "";
7
+ readonly type: "string";
8
+ readonly internalType: "string";
9
+ }];
10
+ readonly stateMutability: "view";
11
+ }, {
12
+ readonly type: "function";
13
+ readonly name: "symbol";
14
+ readonly inputs: readonly [];
15
+ readonly outputs: readonly [{
16
+ readonly name: "";
17
+ readonly type: "string";
18
+ readonly internalType: "string";
19
+ }];
20
+ readonly stateMutability: "view";
21
+ }, {
22
+ readonly type: "function";
23
+ readonly name: "decimals";
24
+ readonly inputs: readonly [];
25
+ readonly outputs: readonly [{
26
+ readonly name: "";
27
+ readonly type: "uint8";
28
+ readonly internalType: "uint8";
29
+ }];
30
+ readonly stateMutability: "view";
31
+ }, {
2
32
  readonly type: "function";
3
33
  readonly name: "balanceOf";
4
34
  readonly inputs: readonly [{
@@ -2,6 +2,27 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ERC20_EVM = void 0;
4
4
  exports.ERC20_EVM = [
5
+ {
6
+ type: 'function',
7
+ name: 'name',
8
+ inputs: [],
9
+ outputs: [{ name: '', type: 'string', internalType: 'string' }],
10
+ stateMutability: 'view',
11
+ },
12
+ {
13
+ type: 'function',
14
+ name: 'symbol',
15
+ inputs: [],
16
+ outputs: [{ name: '', type: 'string', internalType: 'string' }],
17
+ stateMutability: 'view',
18
+ },
19
+ {
20
+ type: 'function',
21
+ name: 'decimals',
22
+ inputs: [],
23
+ outputs: [{ name: '', type: 'uint8', internalType: 'uint8' }],
24
+ stateMutability: 'view',
25
+ },
5
26
  {
6
27
  type: 'function',
7
28
  name: 'balanceOf',
@@ -12,6 +12,6 @@ export { CEA_FACTORY_EVM } from './ceaFactory.evm';
12
12
  export { UNIVERSAL_GATEWAY_PC } from './universalGatewayPC.evm';
13
13
  export { UNIVERSAL_CORE_EVM } from './prc20.evm';
14
14
  export { UEA_FACTORY_ABI } from './uea-factory';
15
- export { IPC20_EVM, PC20_FACTORY_EVM, PC20_WRAPPER_EVM, EXTERNAL_GATEWAY_PC20_EVM, VAULT_PC20_FACTORY_EVM, } from './pc20.evm';
15
+ export { PC20_FACTORY_EVM, PC20_WRAPPER_EVM, EXTERNAL_GATEWAY_PC20_EVM, VAULT_PC20_FACTORY_EVM, } from './pc20.evm';
16
16
  export { WPC_EVM } from './wpc.evm';
17
17
  export { UNIV3_SWAP_ROUTER_EVM } from './uniswap-v3-router.evm';
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.UNIV3_SWAP_ROUTER_EVM = exports.WPC_EVM = exports.VAULT_PC20_FACTORY_EVM = exports.EXTERNAL_GATEWAY_PC20_EVM = exports.PC20_WRAPPER_EVM = exports.PC20_FACTORY_EVM = exports.IPC20_EVM = exports.UEA_FACTORY_ABI = exports.UNIVERSAL_CORE_EVM = exports.UNIVERSAL_GATEWAY_PC = exports.CEA_FACTORY_EVM = exports.CEA_EVM = exports.SVM_GATEWAY_IDL = exports.ERC20_EVM = exports.UNIVERSAL_GATEWAY_V1_SEND = exports.UNIVERSAL_GATEWAY_V0 = exports.UEA_SVM = exports.UEA_EVM = exports.FACTORY_V1 = exports.FEE_LOCKER_SVM = exports.FEE_LOCKER_EVM = void 0;
3
+ exports.UNIV3_SWAP_ROUTER_EVM = exports.WPC_EVM = exports.VAULT_PC20_FACTORY_EVM = exports.EXTERNAL_GATEWAY_PC20_EVM = exports.PC20_WRAPPER_EVM = exports.PC20_FACTORY_EVM = exports.UEA_FACTORY_ABI = exports.UNIVERSAL_CORE_EVM = exports.UNIVERSAL_GATEWAY_PC = exports.CEA_FACTORY_EVM = exports.CEA_EVM = exports.SVM_GATEWAY_IDL = exports.ERC20_EVM = exports.UNIVERSAL_GATEWAY_V1_SEND = exports.UNIVERSAL_GATEWAY_V0 = exports.UEA_SVM = exports.UEA_EVM = exports.FACTORY_V1 = exports.FEE_LOCKER_SVM = exports.FEE_LOCKER_EVM = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  var feeLocker_evm_1 = require("./feeLocker.evm");
6
6
  Object.defineProperty(exports, "FEE_LOCKER_EVM", { enumerable: true, get: function () { return feeLocker_evm_1.FEE_LOCKER_EVM; } });
@@ -33,7 +33,6 @@ var uea_factory_1 = require("./uea-factory");
33
33
  Object.defineProperty(exports, "UEA_FACTORY_ABI", { enumerable: true, get: function () { return uea_factory_1.UEA_FACTORY_ABI; } });
34
34
  // PC20
35
35
  var pc20_evm_1 = require("./pc20.evm");
36
- Object.defineProperty(exports, "IPC20_EVM", { enumerable: true, get: function () { return pc20_evm_1.IPC20_EVM; } });
37
36
  Object.defineProperty(exports, "PC20_FACTORY_EVM", { enumerable: true, get: function () { return pc20_evm_1.PC20_FACTORY_EVM; } });
38
37
  Object.defineProperty(exports, "PC20_WRAPPER_EVM", { enumerable: true, get: function () { return pc20_evm_1.PC20_WRAPPER_EVM; } });
39
38
  Object.defineProperty(exports, "EXTERNAL_GATEWAY_PC20_EVM", { enumerable: true, get: function () { return pc20_evm_1.EXTERNAL_GATEWAY_PC20_EVM; } });
@@ -2,7 +2,6 @@
2
2
  * PC20 ABIs — minimal read surfaces for registry resolution and validation.
3
3
  *
4
4
  * Sources:
5
- * IPC20 push-chain-gateway-contracts/contracts/evm-gateway/src/interfaces/IPC20.sol
6
5
  * PC20Factory push-chain-gateway-contracts/contracts/evm-gateway/src/PC20Factory.sol
7
6
  * PC20Wrapper push-chain-gateway-contracts/contracts/evm-gateway/src/PC20Wrapper.sol
8
7
  * UniversalGateway push-chain-gateway-contracts/contracts/evm-gateway/src/UniversalGateway.sol
@@ -10,30 +9,6 @@
10
9
  * Read-only entries only. Writes go through the existing gateway ABIs — PC20
11
10
  * changes the payload, not the entrypoint.
12
11
  */
13
- /** Push-native token eligible for export. Presence of `pc20Metadata` IS the PC20 test. */
14
- export declare const IPC20_EVM: readonly [{
15
- readonly type: "function";
16
- readonly name: "pc20Metadata";
17
- readonly inputs: readonly [];
18
- readonly outputs: readonly [{
19
- readonly name: "name";
20
- readonly type: "string";
21
- readonly internalType: "string";
22
- }, {
23
- readonly name: "symbol";
24
- readonly type: "string";
25
- readonly internalType: "string";
26
- }, {
27
- readonly name: "decimals";
28
- readonly type: "uint8";
29
- readonly internalType: "uint8";
30
- }, {
31
- readonly name: "originAddress";
32
- readonly type: "address";
33
- readonly internalType: "address";
34
- }];
35
- readonly stateMutability: "view";
36
- }];
37
12
  /**
38
13
  * Destination-chain factory.
39
14
  *
@@ -3,7 +3,6 @@
3
3
  * PC20 ABIs — minimal read surfaces for registry resolution and validation.
4
4
  *
5
5
  * Sources:
6
- * IPC20 push-chain-gateway-contracts/contracts/evm-gateway/src/interfaces/IPC20.sol
7
6
  * PC20Factory push-chain-gateway-contracts/contracts/evm-gateway/src/PC20Factory.sol
8
7
  * PC20Wrapper push-chain-gateway-contracts/contracts/evm-gateway/src/PC20Wrapper.sol
9
8
  * UniversalGateway push-chain-gateway-contracts/contracts/evm-gateway/src/UniversalGateway.sol
@@ -12,22 +11,7 @@
12
11
  * changes the payload, not the entrypoint.
13
12
  */
14
13
  Object.defineProperty(exports, "__esModule", { value: true });
15
- exports.EXTERNAL_GATEWAY_PC20_EVM = exports.VAULT_PC20_FACTORY_EVM = exports.PC20_WRAPPER_EVM = exports.PC20_FACTORY_EVM = exports.IPC20_EVM = void 0;
16
- /** Push-native token eligible for export. Presence of `pc20Metadata` IS the PC20 test. */
17
- exports.IPC20_EVM = [
18
- {
19
- type: 'function',
20
- name: 'pc20Metadata',
21
- inputs: [],
22
- outputs: [
23
- { name: 'name', type: 'string', internalType: 'string' },
24
- { name: 'symbol', type: 'string', internalType: 'string' },
25
- { name: 'decimals', type: 'uint8', internalType: 'uint8' },
26
- { name: 'originAddress', type: 'address', internalType: 'address' },
27
- ],
28
- stateMutability: 'view',
29
- },
30
- ];
14
+ exports.EXTERNAL_GATEWAY_PC20_EVM = exports.VAULT_PC20_FACTORY_EVM = exports.PC20_WRAPPER_EVM = exports.PC20_FACTORY_EVM = void 0;
31
15
  /**
32
16
  * Destination-chain factory.
33
17
  *
@@ -36,6 +36,22 @@ function getViemChain(chain) {
36
36
  }
37
37
  return viemChain;
38
38
  }
39
+ /**
40
+ * Transport for a CEAFactory read.
41
+ *
42
+ * `http(undefined)` falls back to the *viem* chain's default endpoint, which for
43
+ * `sepolia` is `https://sepolia.drpc.org` — a paid-plan host that answers every
44
+ * request with HTTP 400 ("chain is not available on free plan"). That silently
45
+ * broke every CEA derivation that did not pass an explicit rpcUrl, and no caller
46
+ * passes one. Use the SDK's own `CHAIN_INFO[].defaultRPC` list instead, wrapped in
47
+ * `fallback` so one dead endpoint rotates to the next.
48
+ */
49
+ function ceaTransport(chain, rpcUrl) {
50
+ if (rpcUrl)
51
+ return (0, viem_1.http)(rpcUrl);
52
+ const rpcs = chain_1.CHAIN_INFO[chain]?.defaultRPC ?? [];
53
+ return rpcs.length > 0 ? (0, viem_1.fallback)(rpcs.map((url) => (0, viem_1.http)(url))) : (0, viem_1.http)();
54
+ }
39
55
  /**
40
56
  * Get CEA address for a UEA on a specific chain
41
57
  *
@@ -58,7 +74,7 @@ async function getCEAAddress(ueaAddress, chain, rpcUrl) {
58
74
  const viemChain = getViemChain(chain);
59
75
  const client = (0, viem_1.createPublicClient)({
60
76
  chain: viemChain,
61
- transport: (0, viem_1.http)(rpcUrl),
77
+ transport: ceaTransport(chain, rpcUrl),
62
78
  });
63
79
  if (cachedAddress) {
64
80
  // Address is cached — only re-check deployment status
@@ -91,7 +107,7 @@ async function getPushAccountForCEA(ceaAddress, chain, rpcUrl) {
91
107
  const viemChain = getViemChain(chain);
92
108
  const client = (0, viem_1.createPublicClient)({
93
109
  chain: viemChain,
94
- transport: (0, viem_1.http)(rpcUrl),
110
+ transport: ceaTransport(chain, rpcUrl),
95
111
  });
96
112
  const uea = await client.readContract({
97
113
  abi: ceaFactory_evm_1.CEA_FACTORY_EVM,
@@ -118,7 +134,7 @@ async function isCEA(address, chain, rpcUrl) {
118
134
  const viemChain = getViemChain(chain);
119
135
  const client = (0, viem_1.createPublicClient)({
120
136
  chain: viemChain,
121
- transport: (0, viem_1.http)(rpcUrl),
137
+ transport: ceaTransport(chain, rpcUrl),
122
138
  });
123
139
  return client.readContract({
124
140
  abi: ceaFactory_evm_1.CEA_FACTORY_EVM,
@@ -75,7 +75,7 @@ export declare class PC20FactoryMismatchError extends PC20Error {
75
75
  registryFactory?: string;
76
76
  });
77
77
  }
78
- /** Token does not implement `pc20Metadata()`, or the metadata fails validation. */
78
+ /** Token does not expose usable ERC-20 metadata, or the metadata fails validation. */
79
79
  export declare class InvalidPC20MetadataError extends PC20Error {
80
80
  constructor(message: string, ctx?: PC20ErrorContext);
81
81
  }
@@ -108,7 +108,7 @@ class PC20FactoryMismatchError extends PC20Error {
108
108
  }
109
109
  }
110
110
  exports.PC20FactoryMismatchError = PC20FactoryMismatchError;
111
- /** Token does not implement `pc20Metadata()`, or the metadata fails validation. */
111
+ /** Token does not expose usable ERC-20 metadata, or the metadata fails validation. */
112
112
  class InvalidPC20MetadataError extends PC20Error {
113
113
  constructor(message, ctx = {}) {
114
114
  super('PC20_INVALID_METADATA', message, ctx);
@@ -61,6 +61,8 @@ export type PC20GasQuote = {
61
61
  protocolFee: bigint;
62
62
  gasPrice: bigint;
63
63
  chainNamespace: string;
64
+ /** Value encoded in the outbound request before UGPC applies its overhead. */
65
+ requestGasLimit: bigint;
64
66
  gasLimitUsed: bigint;
65
67
  isFirstExport: boolean;
66
68
  /**
@@ -84,10 +86,11 @@ export type PC20GasQuote = {
84
86
  * `pc20DeploymentGasOverhead` and the transfer strands on the destination
85
87
  * with the source token already locked.
86
88
  *
87
- * The second is unrecoverable without operator intervention, so the ceiling
88
- * always covers a deployment. `maxPCForGas` is a ceiling rather than a charge —
89
- * the unspent remainder is not consumed — so the only cost is a higher required
90
- * balance. `gasLimitUsed` itself is passed through unchanged.
89
+ * The second is unrecoverable without operator intervention, so the execution
90
+ * gas limit always covers a deployment. The buffered limit must be re-quoted
91
+ * and written into the outbound request; funding a higher ceiling while leaving
92
+ * the request at the default limit still makes the relayer submit an
93
+ * under-gassed destination transaction.
91
94
  */
92
95
  export declare function quotePC20Export(ctx: OrchestratorContext, pushPC20: `0x${string}`, destinationChain: CHAIN, gasLimit: bigint, opts: PC20ResolverOptions): Promise<PC20GasQuote>;
93
96
  /**
@@ -103,6 +106,7 @@ export declare function queryPC20OutboundGasFee(ctx: OrchestratorContext, pushPC
103
106
  gasFee: bigint;
104
107
  protocolFee: bigint;
105
108
  gasPrice: bigint;
109
+ requestGasLimit: bigint;
106
110
  gasLimitUsed: bigint;
107
111
  universalCoreAddress: `0x${string}`;
108
112
  isFirstExport: boolean;
@@ -47,6 +47,7 @@ exports.queryPC20OutboundGasFee = queryPC20OutboundGasFee;
47
47
  exports.resolveDestinationWrapper = resolveDestinationWrapper;
48
48
  exports.validatePC20Export = validatePC20Export;
49
49
  const viem_1 = require("viem");
50
+ const enums_1 = require("../../../constants/enums");
50
51
  const abi_1 = require("../../../constants/abi");
51
52
  const chain_1 = require("../../../constants/chain");
52
53
  const context_1 = require("../context");
@@ -123,38 +124,55 @@ function assertMetadataFitsFactoryLimits(name, symbol) {
123
124
  * `pc20DeploymentGasOverhead` and the transfer strands on the destination
124
125
  * with the source token already locked.
125
126
  *
126
- * The second is unrecoverable without operator intervention, so the ceiling
127
- * always covers a deployment. `maxPCForGas` is a ceiling rather than a charge —
128
- * the unspent remainder is not consumed — so the only cost is a higher required
129
- * balance. `gasLimitUsed` itself is passed through unchanged.
127
+ * The second is unrecoverable without operator intervention, so the execution
128
+ * gas limit always covers a deployment. The buffered limit must be re-quoted
129
+ * and written into the outbound request; funding a higher ceiling while leaving
130
+ * the request at the default limit still makes the relayer submit an
131
+ * under-gassed destination transaction.
130
132
  */
131
133
  async function quotePC20Export(ctx, pushPC20, destinationChain, gasLimit, opts) {
132
134
  const namespace = (0, chain_namespace_1.chainToNamespace)(destinationChain);
133
135
  const core = await (0, resolver_1.getUniversalCoreAddress)(opts);
134
- const result = await ctx.pushClient.readContract({
136
+ const readQuote = (limit) => ctx.pushClient.readContract({
135
137
  address: core,
136
138
  abi: abi_1.UNIVERSAL_CORE_EVM,
137
139
  functionName: 'getPC20ExportGasAndFees',
138
- args: [namespace, gasLimit, pushPC20],
140
+ args: [namespace, limit, pushPC20],
139
141
  });
140
- const [gasToken, gasFee, protocolFee, gasPrice, chainNamespace, gasLimitUsed, isFirstExport] = result;
141
- const overhead = await ctx.pushClient.readContract({
142
- address: core,
143
- abi: abi_1.UNIVERSAL_CORE_EVM,
144
- functionName: 'pc20DeploymentGasOverhead',
145
- args: [namespace],
146
- });
147
- // Add the overhead even when the quote already included it. Double-counting
148
- // raises a ceiling that is never fully spent; under-counting strands funds.
149
- const gasLimitCeiling = gasLimitUsed + overhead;
150
- (0, context_1.printLog)(ctx, `quotePC20Export — namespace=${chainNamespace}, gasLimitUsed=${gasLimitUsed}, ` +
151
- `isFirstExport=${isFirstExport}, overhead=${overhead}, ceiling=${gasLimitCeiling}`);
142
+ const [baseQuote, overhead] = await Promise.all([
143
+ readQuote(gasLimit),
144
+ ctx.pushClient.readContract({
145
+ address: core,
146
+ abi: abi_1.UNIVERSAL_CORE_EVM,
147
+ functionName: 'pc20DeploymentGasOverhead',
148
+ args: [namespace],
149
+ }),
150
+ ]);
151
+ const baseGasLimitUsed = baseQuote[5];
152
+ const isFirstExport = baseQuote[6];
153
+ // Feed the resolved first quote back as the explicit request limit. UGPC
154
+ // applies its deployment overhead to that value, giving one additional
155
+ // safety margin while keeping the fee quote and final destination limit in
156
+ // sync. Passing `gasLimitCeiling` here would make UGPC add the overhead yet
157
+ // again and over-provision the relayer transaction.
158
+ const gasLimitCeiling = baseGasLimitUsed + overhead;
159
+ const isEvmDestination = (0, chain_namespace_1.vmForChain)(destinationChain) === enums_1.VM.EVM;
160
+ const requestGasLimit = isEvmDestination ? baseGasLimitUsed : gasLimit;
161
+ // Solana has a separate rent/compute-budget requote path; preserve its base
162
+ // quote here so this EVM wrapper-deployment guard does not double-count it.
163
+ const bufferedQuote = isEvmDestination
164
+ ? await readQuote(requestGasLimit)
165
+ : baseQuote;
166
+ const [gasToken, gasFee, protocolFee, gasPrice, chainNamespace, gasLimitUsed] = bufferedQuote;
167
+ (0, context_1.printLog)(ctx, `quotePC20Export — namespace=${chainNamespace}, baseGasLimit=${baseGasLimitUsed}, ` +
168
+ `isFirstExport=${isFirstExport}, overhead=${overhead}, executionGasLimit=${gasLimitUsed}`);
152
169
  return {
153
170
  gasToken,
154
171
  gasFee,
155
172
  protocolFee,
156
173
  gasPrice,
157
174
  chainNamespace,
175
+ requestGasLimit,
158
176
  gasLimitUsed,
159
177
  isFirstExport,
160
178
  gasLimitCeiling,
@@ -180,6 +198,7 @@ async function queryPC20OutboundGasFee(ctx, pushPC20, gasLimit, destinationChain
180
198
  gasFee: quote.gasFee,
181
199
  protocolFee: quote.protocolFee,
182
200
  gasPrice: quote.gasPrice,
201
+ requestGasLimit: quote.requestGasLimit,
183
202
  gasLimitUsed: quote.gasLimitUsed,
184
203
  universalCoreAddress,
185
204
  isFirstExport: quote.isFirstExport,
@@ -36,12 +36,11 @@
36
36
  */
37
37
  import { type Hex } from 'viem';
38
38
  import { CHAIN, PUSH_NETWORK, VM } from '../../../constants/enums';
39
- /** Metadata read off a Push-native PC20 via `pc20Metadata()`. */
39
+ /** Standard ERC-20 metadata forwarded when exporting a Push-native PC20. */
40
40
  export type PC20Metadata = {
41
41
  name: string;
42
42
  symbol: string;
43
43
  decimals: number;
44
- originAddress: `0x${string}`;
45
44
  };
46
45
  /** A confirmed wrapper deployment on one external chain. */
47
46
  export type PC20DeploymentInfo = {
@@ -122,12 +121,11 @@ export declare function resolveWrapperToSource(chain: CHAIN, address: string, op
122
121
  */
123
122
  export declare function verifyEvmWrapperIdentity(chain: CHAIN, address: string, pushAddress: `0x${string}`, opts: PC20ResolverOptions): Promise<void>;
124
123
  /**
125
- * Read and validate `pc20Metadata()` on a Push-native token.
124
+ * Read and validate standard ERC-20 metadata on a Push-native token.
126
125
  *
127
- * The absence of `pc20Metadata` is the discriminator between a PC20 and any
128
- * other ERC20 — never the symbol. A synthetic PRC20 is detected separately so
129
- * it gets an error that points at the right API rather than reading as
130
- * "your token is broken".
126
+ * Every metadata-compatible ERC-20 born on Push Chain is eligible for PC20
127
+ * export. Synthetic PRC20s expose `CHAIN_NAMESPACE()` and are rejected even
128
+ * though they also implement the standard metadata functions.
131
129
  */
132
130
  export declare function readPushPC20Metadata(pushAddress: `0x${string}`, opts: PC20ResolverOptions): Promise<PC20Metadata>;
133
131
  /**
@@ -111,10 +111,9 @@ function clientFor(chain, opts) {
111
111
  * dispatches them synchronously, which is what puts them in the same batch
112
112
  * window — awaiting them one at a time would silently become N round trips.
113
113
  *
114
- * Failures are captured per call rather than thrown, because several PC20 probes
115
- * are *expected* to revert: `pc20Metadata()` reverting on a plain ERC20 is the
116
- * primary discriminator, not an error condition. Callers interpret each result
117
- * individually via {@link unwrap}.
114
+ * Failures are captured per call rather than thrown, because optional identity
115
+ * probes such as `CHAIN_NAMESPACE()` are expected to revert on ordinary ERC-20s.
116
+ * Callers interpret each result individually via {@link unwrap}.
118
117
  */
119
118
  async function batchRead(client, contracts) {
120
119
  exports.__readCount.n += 1;
@@ -376,12 +375,11 @@ async function verifyEvmWrapperIdentity(chain, address, pushAddress, opts) {
376
375
  // Push-native metadata
377
376
  // ---------------------------------------------------------------------------
378
377
  /**
379
- * Read and validate `pc20Metadata()` on a Push-native token.
378
+ * Read and validate standard ERC-20 metadata on a Push-native token.
380
379
  *
381
- * The absence of `pc20Metadata` is the discriminator between a PC20 and any
382
- * other ERC20 — never the symbol. A synthetic PRC20 is detected separately so
383
- * it gets an error that points at the right API rather than reading as
384
- * "your token is broken".
380
+ * Every metadata-compatible ERC-20 born on Push Chain is eligible for PC20
381
+ * export. Synthetic PRC20s expose `CHAIN_NAMESPACE()` and are rejected even
382
+ * though they also implement the standard metadata functions.
385
383
  */
386
384
  async function readPushPC20Metadata(pushAddress, opts) {
387
385
  const pushChain = (0, helpers_1.getPushChainForNetwork)(opts.network);
@@ -390,10 +388,26 @@ async function readPushPC20Metadata(pushAddress, opts) {
390
388
  if (cached)
391
389
  return cached;
392
390
  const client = clientFor(pushChain, opts);
393
- const [metaEntry, codeEntry] = await Promise.all([
391
+ const [metadataEntries, codeEntry] = await Promise.all([
394
392
  batchRead(client, [
395
- { address: pushAddress, abi: abi_1.IPC20_EVM, functionName: 'pc20Metadata', args: [] },
396
- ]).then((r) => r[0]),
393
+ { address: pushAddress, abi: abi_1.ERC20_EVM, functionName: 'name', args: [] },
394
+ { address: pushAddress, abi: abi_1.ERC20_EVM, functionName: 'symbol', args: [] },
395
+ { address: pushAddress, abi: abi_1.ERC20_EVM, functionName: 'decimals', args: [] },
396
+ {
397
+ address: pushAddress,
398
+ abi: [
399
+ {
400
+ type: 'function',
401
+ name: 'CHAIN_NAMESPACE',
402
+ inputs: [],
403
+ outputs: [{ name: '', type: 'string' }],
404
+ stateMutability: 'view',
405
+ },
406
+ ],
407
+ functionName: 'CHAIN_NAMESPACE',
408
+ args: [],
409
+ },
410
+ ]),
397
411
  (async () => {
398
412
  exports.__readCount.n += 1;
399
413
  return { status: 'success', result: await client.getCode({ address: pushAddress }) };
@@ -406,22 +420,19 @@ async function readPushPC20Metadata(pushAddress, opts) {
406
420
  address: pushAddress,
407
421
  });
408
422
  }
409
- const meta = unwrap(metaEntry);
410
- if (!meta) {
411
- if (await isSyntheticPRC20(pushAddress, opts)) {
412
- throw new errors_1.PC20ExpectedButPRC20Error({
413
- chain: String(pushChain),
414
- address: pushAddress,
415
- });
416
- }
417
- throw new errors_1.InvalidPC20MetadataError('Token does not implement pc20Metadata() and is not a Push-native PC20.', { chain: String(pushChain), address: pushAddress });
423
+ const prc20Namespace = unwrap(metadataEntries[3]);
424
+ if (typeof prc20Namespace === 'string') {
425
+ throw new errors_1.PC20ExpectedButPRC20Error({
426
+ chain: String(pushChain),
427
+ address: pushAddress,
428
+ chainNamespace: prc20Namespace,
429
+ });
418
430
  }
419
- const [name, symbol, decimals, originAddress] = meta;
420
- // The origin binding is what makes the metadata trustworthy: a contract can
421
- // return any name it likes, but claiming to originate at a different address
422
- // means it is not the canonical source for this token.
423
- if ((0, viem_1.getAddress)(originAddress) !== (0, viem_1.getAddress)(pushAddress)) {
424
- throw new errors_1.InvalidPC20MetadataError(`pc20Metadata().originAddress (${originAddress}) does not match the token address.`, { chain: String(pushChain), address: pushAddress });
431
+ const name = unwrap(metadataEntries[0]);
432
+ const symbol = unwrap(metadataEntries[1]);
433
+ const decimals = unwrap(metadataEntries[2]);
434
+ if (name === undefined || symbol === undefined || decimals === undefined) {
435
+ throw new errors_1.InvalidPC20MetadataError('Token must implement the ERC-20 metadata functions name(), symbol(), and decimals().', { chain: String(pushChain), address: pushAddress });
425
436
  }
426
437
  if (!name || !symbol) {
427
438
  throw new errors_1.InvalidPC20MetadataError('PC20 name and symbol must be non-empty.', {
@@ -429,41 +440,21 @@ async function readPushPC20Metadata(pushAddress, opts) {
429
440
  address: pushAddress,
430
441
  });
431
442
  }
443
+ const normalizedDecimals = Number(decimals);
444
+ if (!Number.isInteger(normalizedDecimals) || normalizedDecimals < 0 || normalizedDecimals > 255) {
445
+ throw new errors_1.InvalidPC20MetadataError('PC20 decimals must be a valid uint8 value.', {
446
+ chain: String(pushChain),
447
+ address: pushAddress,
448
+ });
449
+ }
432
450
  const validated = {
433
451
  name,
434
452
  symbol,
435
- decimals: Number(decimals),
436
- originAddress: (0, viem_1.getAddress)(originAddress),
453
+ decimals: normalizedDecimals,
437
454
  };
438
455
  metadataCache.set(key, validated);
439
456
  return validated;
440
457
  }
441
- /**
442
- * Distinguish a synthetic PRC20 from an arbitrary ERC20.
443
- *
444
- * PRC20s expose the origin chain they wrap; PC20s do not. Checked only after
445
- * `pc20Metadata()` has already failed, so it costs nothing on the happy path.
446
- */
447
- async function isSyntheticPRC20(address, opts) {
448
- const client = clientFor((0, helpers_1.getPushChainForNetwork)(opts.network), opts);
449
- const [entry] = await batchRead(client, [
450
- {
451
- address,
452
- abi: [
453
- {
454
- type: 'function',
455
- name: 'CHAIN_NAMESPACE',
456
- inputs: [],
457
- outputs: [{ name: '', type: 'string' }],
458
- stateMutability: 'view',
459
- },
460
- ],
461
- functionName: 'CHAIN_NAMESPACE',
462
- args: [],
463
- },
464
- ]);
465
- return typeof unwrap(entry) === 'string';
466
- }
467
458
  // ---------------------------------------------------------------------------
468
459
  // Destination-side lookups
469
460
  // ---------------------------------------------------------------------------
@@ -487,6 +487,7 @@ async function executeUoaToCea(ctx, params, executeFn) {
487
487
  (0, context_1.printLog)(ctx, `executeUoaToCea — PAYLOAD-only: using native PRC-20 ${prc20Token} for chain ${targetChain} with zero burn amount`);
488
488
  }
489
489
  const gasLimitForQuery = params.gasLimit ?? BigInt(0);
490
+ let outboundGasLimit = gasLimitForQuery;
490
491
  // --- 202: Gas estimation (spec-ordered before 203) ---
491
492
  let gasFee = BigInt(0);
492
493
  let protocolFee = BigInt(0);
@@ -499,9 +500,20 @@ async function executeUoaToCea(ctx, params, executeFn) {
499
500
  // getPC20ExportGasAndFees accounts for the first-export deployment
500
501
  // overhead, and quoting the wrong one strands the destination transfer.
501
502
  const pc20Export = params._pc20;
502
- const result = pc20Export?.direction === 'export'
503
- ? await (0, export_1.queryPC20OutboundGasFee)(ctx, prc20Token, gasLimitForQuery, targetChain)
504
- : await (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, gasLimitForQuery, targetChain);
503
+ const isPc20ExportQuote = pc20Export?.direction === 'export';
504
+ let result;
505
+ if (isPc20ExportQuote) {
506
+ const pc20Quote = await (0, export_1.queryPC20OutboundGasFee)(ctx, prc20Token, gasLimitForQuery, targetChain);
507
+ result = pc20Quote;
508
+ // PC20 first exports deploy a wrapper on the destination. The quote is
509
+ // re-issued with that deployment margin, and the relayer must receive
510
+ // the corresponding explicit request limit—not the caller's original
511
+ // zero/default.
512
+ outboundGasLimit = pc20Quote.requestGasLimit;
513
+ }
514
+ else {
515
+ result = await (0, gas_calculator_1.queryOutboundGasFee)(ctx, prc20Token, gasLimitForQuery, targetChain);
516
+ }
505
517
  gasFee = result.gasFee;
506
518
  protocolFee = result.protocolFee;
507
519
  gasToken = result.gasToken;
@@ -567,7 +579,7 @@ async function executeUoaToCea(ctx, params, executeFn) {
567
579
  destinationUserData: ceaPayload,
568
580
  })
569
581
  : ceaPayload;
570
- const outboundReq = (0, payload_builders_2.buildOutboundRequest)(targetBytes, prc20Token, burnAmount, gasLimitForQuery, outboundPayload, ueaAddress, // revert recipient is the UEA
582
+ const outboundReq = (0, payload_builders_2.buildOutboundRequest)(targetBytes, prc20Token, burnAmount, outboundGasLimit, outboundPayload, ueaAddress, // revert recipient is the UEA
571
583
  params.maxPCForGas ?? BigInt(0));
572
584
  (0, context_1.printLog)(ctx, `executeUoaToCea — outbound request: ${JSON.stringify({
573
585
  recipient: outboundReq.recipient,