fix: Asset Hub AccountId32 encoding for withdrawal edge functions

Deno npm shim breaks SS58 decoding in @pezkuwi/api type registry,
causing PezspCoreCryptoAccountId32 to receive 48-byte SS58 strings
instead of 32-byte public keys. Added inline ss58ToHex decoder and
explicit hex-based nonce fetching to avoid all SS58 → AccountId32
conversions at the API level. Also adds P2P E2E test script (45/45).
This commit is contained in:
2026-02-24 00:16:11 +03:00
parent d40647aa50
commit cc986b4ed7
3 changed files with 546 additions and 13 deletions
@@ -7,6 +7,30 @@ import { createClient } from 'npm:@supabase/supabase-js@2'
import { ApiPromise, WsProvider, Keyring } from 'npm:@pezkuwi/api@16.5.11'
import { cryptoWaitReady } from 'npm:@pezkuwi/util-crypto@14.0.11'
// Decode SS58 address to raw 32-byte public key hex
function ss58ToHex(address: string): string {
const CHARS = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
let leadingZeros = 0
for (const c of address) {
if (c !== '1') break
leadingZeros++
}
let num = 0n
for (const c of address) {
num = num * 58n + BigInt(CHARS.indexOf(c))
}
const hex = num.toString(16)
const paddedHex = hex.length % 2 ? '0' + hex : hex
const decoded = new Uint8Array(leadingZeros + paddedHex.length / 2)
for (let i = 0; i < leadingZeros; i++) decoded[i] = 0
for (let i = 0; i < paddedHex.length / 2; i++) {
decoded[leadingZeros + i] = parseInt(paddedHex.slice(i * 2, i * 2 + 2), 16)
}
// SS58: [1-byte prefix] [32 bytes pubkey] [2 bytes checksum]
const pubkey = decoded.slice(1, 33)
return '0x' + Array.from(pubkey, (b: number) => b.toString(16).padStart(2, '0')).join('')
}
// Allowed origins for CORS
const ALLOWED_ORIGINS = [
'https://app.pezkuwichain.io',
@@ -95,23 +119,29 @@ async function sendTokens(
// Convert amount to chain units
const amountBN = BigInt(Math.floor(amount * Math.pow(10, DECIMALS)))
// Build transaction
// Convert all addresses to hex (Deno npm shim breaks SS58 decoding in @pezkuwi/api types)
const destHex = ss58ToHex(toAddress)
const signerHex = '0x' + Array.from(hotWallet.publicKey, (b: number) => b.toString(16).padStart(2, '0')).join('')
console.log(`Sending ${amount} ${token}: ${signerHex}${destHex}`)
let tx
if (token === 'HEZ') {
// Native token transfer
tx = api.tx.balances.transferKeepAlive(toAddress, amountBN)
tx = api.tx.balances.transferKeepAlive({ Id: destHex }, amountBN)
} else if (token === 'PEZ') {
// Asset transfer
tx = api.tx.assets.transfer(PEZ_ASSET_ID, toAddress, amountBN)
tx = api.tx.assets.transfer(PEZ_ASSET_ID, { Id: destHex }, amountBN)
} else {
return { success: false, error: 'Invalid token' }
}
// Fetch nonce via hex pubkey to avoid SS58 → AccountId32 decoding issue
const accountInfo = await api.query.system.account(signerHex)
const nonce = accountInfo.nonce
// Sign and send transaction
return new Promise((resolve) => {
let txHash: string
tx.signAndSend(hotWallet, { nonce: -1 }, (result) => {
tx.signAndSend(hotWallet, { nonce }, (result) => {
txHash = result.txHash.toHex()
if (result.status.isInBlock) {
@@ -24,6 +24,29 @@ import { createClient } from 'npm:@supabase/supabase-js@2'
import { ApiPromise, WsProvider, Keyring } from 'npm:@pezkuwi/api@16.5.11'
import { cryptoWaitReady } from 'npm:@pezkuwi/util-crypto@14.0.11'
// Decode SS58 address to raw 32-byte public key hex
function ss58ToHex(address: string): string {
const CHARS = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
let leadingZeros = 0;
for (const c of address) {
if (c !== '1') break;
leadingZeros++;
}
let num = 0n;
for (const c of address) {
num = num * 58n + BigInt(CHARS.indexOf(c));
}
const hex = num.toString(16);
const paddedHex = hex.length % 2 ? '0' + hex : hex;
const decoded = new Uint8Array(leadingZeros + paddedHex.length / 2);
for (let i = 0; i < leadingZeros; i++) decoded[i] = 0;
for (let i = 0; i < paddedHex.length / 2; i++) {
decoded[leadingZeros + i] = parseInt(paddedHex.slice(i * 2, i * 2 + 2), 16);
}
const pubkey = decoded.slice(1, 33);
return '0x' + Array.from(pubkey, (b: number) => b.toString(16).padStart(2, '0')).join('');
}
// Configuration
const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!;
const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
@@ -68,25 +91,31 @@ async function processWithdrawal(
// 2. Calculate amount in planck (smallest unit)
const amountPlanck = BigInt(Math.floor(amount * Math.pow(10, DECIMALS)));
// 3. Build transaction based on token type
// 3. Convert addresses to hex (Deno npm shim breaks SS58 decoding in @pezkuwi/api types)
const destHex = ss58ToHex(wallet_address);
const signerHex = '0x' + Array.from(platformWallet.publicKey, (b: number) => b.toString(16).padStart(2, '0')).join('');
console.log(`Sending ${amount} ${token}: ${signerHex}${destHex}`);
let tx;
if (token === "HEZ" || ASSET_IDS[token] === null) {
// Native token transfer
tx = api.tx.balances.transferKeepAlive(wallet_address, amountPlanck);
tx = api.tx.balances.transferKeepAlive({ Id: destHex }, amountPlanck);
} else {
// Asset transfer
const assetId = ASSET_IDS[token];
if (assetId === undefined) {
throw new Error(`Unknown token: ${token}`);
}
tx = api.tx.assets.transfer(assetId, wallet_address, amountPlanck);
tx = api.tx.assets.transfer(assetId, { Id: destHex }, amountPlanck);
}
// 4. Sign and send transaction
// 4. Fetch nonce via hex pubkey to avoid SS58 → AccountId32 decoding issue
const accountInfo = await api.query.system.account(signerHex);
const nonce = accountInfo.nonce;
// 5. Sign and send transaction
const txHash = await new Promise<string>((resolve, reject) => {
let unsubscribe: () => void;
tx.signAndSend(platformWallet, { nonce: -1 }, ({ status, dispatchError }) => {
tx.signAndSend(platformWallet, { nonce }, ({ status, dispatchError }) => {
if (dispatchError) {
if (dispatchError.isModule) {
const decoded = api.registry.findMetaError(dispatchError.asModule);