Skip to content

Commit

Permalink
ibc: transparent address support (#1950)
Browse files Browse the repository at this point in the history
* add t-addr support to view service

* backwards compatibility with existing frontends

* changeset

* fix inbound / outbound ibc transfers

* linting

* remove deprecated useCompatAddress field

* fix wasm test

* use ephemeral address format for ibc-in

* bech32 shared helper

* comment: bech32, rather thana bech32m

* timeout msg

* remove compat address format from bech32 and types package

* use t-addr field in ics20 withdrawal component

* update changeset

* add compat and t-addr bech32 encoding

* preprocessing the transaction view

* linting

* appease linter

* linting rule
  • Loading branch information
TalDerei authored Dec 22, 2024
1 parent 79d1953 commit 95d5fd9
Show file tree
Hide file tree
Showing 23 changed files with 300 additions and 89 deletions.
10 changes: 10 additions & 0 deletions .changeset/perfect-colts-change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@penumbra-zone/protobuf': major
'@penumbra-zone/services': minor
'@penumbra-zone/bech32m': minor
'minifront': minor
'@penumbra-zone/types': minor
'@penumbra-zone/wasm': minor
---

support transparent addresses for usdc noble IBC withdrawals
30 changes: 20 additions & 10 deletions apps/minifront/src/state/ibc-in/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import { getAddrByIndex } from '../../fetchers/address';
import { bech32mAddress } from '@penumbra-zone/bech32m/penumbra';
import { Toast } from '@penumbra-zone/ui-deprecated/lib/toast/toast';
import { shorten } from '@penumbra-zone/types/string';
import { Address } from '@penumbra-zone/protobuf/penumbra/core/keys/v1/keys_pb';
import { bech32CompatAddress } from '@penumbra-zone/bech32m/penumbracompat1';
import { calculateFee, GasPrice, SigningStargateClient } from '@cosmjs/stargate';
import { chains } from 'chain-registry';
import { getChainId } from '../../fetchers/chain-id';
Expand All @@ -19,9 +17,10 @@ import { currentTimePlusTwoDaysRounded } from '../ibc-out';
import { EncodeObject } from '@cosmjs/proto-signing';
import { MsgTransfer } from 'cosmjs-types/ibc/applications/transfer/v1/tx';
import { parseRevisionNumberFromChainId } from './parse-revision-number-from-chain-id';
import { bech32ChainIds } from '../shared.ts';
import { penumbra } from '../../penumbra.ts';
import { TendermintProxyService } from '@penumbra-zone/protobuf';
import { TendermintProxyService, ViewService } from '@penumbra-zone/protobuf';
import { TransparentAddressRequest } from '@penumbra-zone/protobuf/penumbra/view/v1/view_pb';
import { bech32ChainIds } from '../shared.ts';

export interface IbcInSlice {
selectedChain?: ChainInfo;
Expand Down Expand Up @@ -135,10 +134,6 @@ const getExplorerPage = (txHash: string, chainId?: string) => {
return txPage.replace('${txHash}', txHash);
};

const getCompatibleBech32 = (chainId: string, address: Address): string => {
return bech32ChainIds.includes(chainId) ? bech32CompatAddress(address) : bech32mAddress(address);
};

export const getPenumbraAddress = async (
account: number,
chainId?: string,
Expand All @@ -147,7 +142,7 @@ export const getPenumbraAddress = async (
return undefined;
}
const receiverAddress = await getAddrByIndex(account, true);
return getCompatibleBech32(chainId, receiverAddress);
return bech32mAddress(receiverAddress);
};

const estimateFee = async ({
Expand Down Expand Up @@ -198,7 +193,7 @@ async function execute(
throw new Error('Penumbra chain id could not be retrieved');
}

const penumbraAddress = await getPenumbraAddress(account, selectedChain.chainId);
let penumbraAddress = await getPenumbraAddress(account, selectedChain.chainId);
if (!penumbraAddress) {
throw new Error('Penumbra address not available');
}
Expand All @@ -207,6 +202,21 @@ async function execute(
const assetMetadata = augmentToAsset(coin.raw.denom, selectedChain.chainName);

const transferToken = fromDisplayAmount(assetMetadata, coin.displayDenom, amount);

const { address: t_addr, encoding: encoding } = await penumbra
.service(ViewService)
.transparentAddress(new TransparentAddressRequest({}));
if (!t_addr) {
throw new Error('Error with generating IBC transparent address');
}

// Temporary: detect USDC Noble inbound transfers, and use a transparent (t-addr) encoding
// to ensure Bech32 encoding compatibility.
if (transferToken.denom.includes('uusdc') && bech32ChainIds.includes(selectedChain.chainId)) {
// Set the reciever address to the t-addr encoding.
penumbraAddress = encoding;
}

const params: MsgTransfer = {
sourcePort: 'transfer',
sourceChannel: await getCounterpartyChannelId(selectedChain, penumbraChainId),
Expand Down
29 changes: 25 additions & 4 deletions apps/minifront/src/state/ibc-out.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { AllSlices, SliceCreator, useStore } from '.';
import {
BalancesResponse,
TransactionPlannerRequest,
TransparentAddressRequest,
} from '@penumbra-zone/protobuf/penumbra/view/v1/view_pb';
import { BigNumber } from 'bignumber.js';
import { ClientState } from '@penumbra-zone/protobuf/ibc/lightclients/tendermint/v1/tendermint_pb';
Expand All @@ -24,14 +25,14 @@ import { Channel } from '@penumbra-zone/protobuf/ibc/core/channel/v1/channel_pb'
import { BLOCKS_PER_HOUR } from './constants';
import { createZQuery, ZQueryState } from '@penumbra-zone/zquery';
import { getChains } from '../fetchers/registry';
import { bech32ChainIds } from './shared';
import { penumbra } from '../penumbra';
import {
IbcChannelService,
IbcClientService,
IbcConnectionService,
ViewService,
} from '@penumbra-zone/protobuf';
import { bech32ChainIds } from './shared';

export const { chains, useChains } = createZQuery({
name: 'chains',
Expand Down Expand Up @@ -206,7 +207,7 @@ const getPlanRequest = async ({
}

const addressIndex = getAddressIndex(selection.accountAddress);
const { address: returnAddress } = await penumbra
let { address: returnAddress } = await penumbra
.service(ViewService)
.ephemeralAddress({ addressIndex });
if (!returnAddress) {
Expand All @@ -215,20 +216,40 @@ const getPlanRequest = async ({

const { timeoutHeight, timeoutTime } = await getTimeout(chain.channelId);

// Request transparent address from view service
const { address: t_addr } = await penumbra
.service(ViewService)
.transparentAddress(new TransparentAddressRequest({}));
if (!t_addr) {
throw new Error('Error with generating IBC transparent address');
}

// IBC-related fields
const denom = getMetadata(selection.balanceView).base;
let useTransparentAddress = false;

// Temporary: detect USDC Noble withdrawals, and use a transparent (t-addr) return
// address to ensure Bech32 encoding compatibility.
if (denom.includes('uusdc') && bech32ChainIds.includes(chain.chainId)) {
// Outbound IBC transfers timeout without setting either of these fields.
useTransparentAddress = true;
returnAddress = t_addr;
}

return new TransactionPlannerRequest({
ics20Withdrawals: [
{
amount: toBaseUnit(
BigNumber(amount),
getDisplayDenomExponentFromValueView(selection.balanceView),
),
denom: { denom: getMetadata(selection.balanceView).base },
denom: { denom },
destinationChainAddress,
returnAddress,
timeoutHeight,
timeoutTime,
sourceChannel: chain.channelId,
useCompatAddress: bech32ChainIds.includes(chain.chainId),
useTransparentAddress,
},
],
source: addressIndex,
Expand Down
1 change: 1 addition & 0 deletions packages/bech32m/src/format/bytes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ export const ByteLength = {
penumbravalid: 32,
penumbrawalletid: 32,
plpid: 32,
tpenumbra: 32,
} as const satisfies Required<Record<Prefix, number>>;
18 changes: 12 additions & 6 deletions packages/bech32m/src/format/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,6 @@ export default {
byteLength: ByteLength.penumbra,
innerName: Inner.penumbra,
},
penumbracompat1: {
prefix: Prefixes.penumbracompat1,
stringLength: StringLength.penumbracompat1,
byteLength: ByteLength.penumbracompat1,
innerName: Inner.penumbracompat1,
},
penumbrafullviewingkey: {
prefix: Prefixes.penumbrafullviewingkey,
stringLength: StringLength.penumbrafullviewingkey,
Expand Down Expand Up @@ -73,4 +67,16 @@ export default {
byteLength: ByteLength.plpid,
innerName: Inner.plpid,
},
penumbracompat1: {
prefix: Prefixes.penumbracompat1,
stringLength: StringLength.penumbracompat1,
byteLength: ByteLength.penumbracompat1,
innerName: Inner.penumbracompat1,
},
tpenumbra: {
prefix: Prefixes.tpenumbra,
stringLength: StringLength.tpenumbra,
byteLength: ByteLength.tpenumbra,
innerName: Inner.tpenumbra,
},
} as const satisfies PenumbraBech32mSpec;
3 changes: 2 additions & 1 deletion packages/bech32m/src/format/inner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ export const Inner = {
passet: 'inner',
pauctid: 'inner',
penumbra: 'inner',
penumbracompat1: 'inner',
penumbrafullviewingkey: 'inner',
penumbragovern: 'gk',
penumbraspendkey: 'inner',
penumbravalid: 'ik',
penumbrawalletid: 'inner',
plpid: 'inner',
penumbracompat1: 'inner',
tpenumbra: 'inner',
} as const satisfies Required<Record<Prefix, string>>;
3 changes: 2 additions & 1 deletion packages/bech32m/src/format/prefix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ export const Prefixes = {
passet: 'passet',
pauctid: 'pauctid',
penumbra: 'penumbra',
penumbracompat1: 'penumbracompat1',
penumbrafullviewingkey: 'penumbrafullviewingkey',
penumbragovern: 'penumbragovern',
penumbraspendkey: 'penumbraspendkey',
penumbravalid: 'penumbravalid',
penumbrawalletid: 'penumbrawalletid',
plpid: 'plpid',
penumbracompat1: 'penumbracompat1',
tpenumbra: 'tpenumbra',
} as const;

export type Prefix = keyof typeof Prefixes;
3 changes: 2 additions & 1 deletion packages/bech32m/src/format/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ export const StringLength = {
passet: 65,
pauctid: 66,
penumbra: 143,
penumbracompat1: 150,
penumbrafullviewingkey: 132,
penumbragovern: 73,
penumbraspendkey: 75,
penumbravalid: 72,
penumbrawalletid: 75,
plpid: 64,
penumbracompat1: 150,
tpenumbra: 68,
} as const satisfies Required<Record<Prefix, number>>;
3 changes: 3 additions & 0 deletions packages/bech32m/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ export const PENUMBRA_BECH32M_WALLETID_PREFIX = SPEC.penumbrawalletid.prefix;

export const PENUMBRA_BECH32M_POSITIONID_LENGTH = SPEC.plpid.stringLength;
export const PENUMBRA_BECH32M_POSITIONID_PREFIX = SPEC.plpid.prefix;

export const PENUMBRA_BECH32M_TRANSPARENT_LENGTH = SPEC.tpenumbra.stringLength;
export const PENUMBRA_BECH32M_TRANSPARENT_PREFIX = SPEC.tpenumbra.prefix;
22 changes: 22 additions & 0 deletions packages/bech32m/src/test/tpenumbra.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { describe } from 'vitest';
import { generateTests } from './util/generate-tests.js';
import { bech32TransparentAddress, transparentAddressFromBech32 } from '../tpenumbra.js';
import { Prefixes } from '../format/prefix.js';
import { Inner } from '../format/inner.js';

describe('transparent address conversion', () => {
const okInner = new Uint8Array([
102, 236, 169, 166, 203, 152, 194, 89, 236, 246, 59, 69, 221, 32, 49, 49, 83, 29, 119, 117, 124,
201, 194, 156, 219, 251, 137, 202, 157, 235, 1, 15,
]);
const okBech32 = 'tpenumbra1vmk2nfktnrp9nm8k8dza6gp3x9f36am40nyu98xmlwyu480tqy8sr3jfzd';

generateTests(
Prefixes.tpenumbra,
Inner.tpenumbra,
okInner,
okBech32,
bech32TransparentAddress,
transparentAddressFromBech32,
);
});
27 changes: 27 additions & 0 deletions packages/bech32m/src/tpenumbra.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { fromBech32, toBech32 } from './format/convert.js';
import { Inner } from './format/inner.js';
import { Prefixes } from './format/prefix.js';

const innerName = Inner.tpenumbra;
const prefix = Prefixes.tpenumbra;

export const bech32TransparentAddress = ({ [innerName]: bytes }: { [innerName]: Uint8Array }) =>
toBech32(bytes, prefix);

export const transparentAddressFromBech32 = (penumbra1: string): { [innerName]: Uint8Array } => ({
[innerName]: fromBech32(penumbra1 as `${typeof prefix}1${string}`, prefix),
});

export const isAddress = (check: string): check is `${typeof prefix}1${string}` => {
try {
transparentAddressFromBech32(check);
return true;
} catch {
return false;
}
};

export {
PENUMBRA_BECH32M_TRANSPARENT_LENGTH,
PENUMBRA_BECH32M_TRANSPARENT_PREFIX,
} from './index.js';
2 changes: 1 addition & 1 deletion packages/protobuf/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"gen:ibc": "buf generate buf.build/cosmos/ibc:7ab44ae956a0488ea04e04511efa5f70",
"gen:ics23": "buf generate buf.build/cosmos/ics23:55085f7c710a45f58fa09947208eb70b",
"gen:noble": "buf generate buf.build/noble-assets/forwarding:5a8609a6772d417584a9c60cd8b80881",
"gen:penumbra": "buf generate buf.build/penumbra-zone/penumbra:37cef73133644d9dbdeae95b644db3ec",
"gen:penumbra": "buf generate buf.build/penumbra-zone/penumbra:0a56a4f32c244e7eb277e02f6e85afbd",
"lint": "eslint src",
"lint:fix": "eslint src --fix",
"lint:strict": "tsc --noEmit && eslint src --max-warnings 0",
Expand Down
2 changes: 2 additions & 0 deletions packages/services/src/view-service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { unclaimedSwaps } from './unclaimed-swaps.js';
import { walletId } from './wallet-id.js';
import { witness } from './witness.js';
import { witnessAndBuild } from './witness-and-build.js';
import { transparentAddress } from './transparent-address.js';

export type Impl = ServiceImpl<typeof ViewService>;

Expand Down Expand Up @@ -62,4 +63,5 @@ export const viewImpl: Impl = {
walletId,
witness,
witnessAndBuild,
transparentAddress,
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Code, ConnectError } from '@connectrpc/connect';
import { generateTransactionInfo } from '@penumbra-zone/wasm/transaction';
import { TransactionInfo } from '@penumbra-zone/protobuf/penumbra/view/v1/view_pb';
import { fvkCtx } from '../ctx/full-viewing-key.js';
import { txvTranslator } from './util/transaction-view.js';

export const transactionInfoByHash: Impl['transactionInfoByHash'] = async (req, ctx) => {
if (!req.id) {
Expand All @@ -23,11 +24,15 @@ export const transactionInfoByHash: Impl['transactionInfoByHash'] = async (req,
throw new ConnectError('Transaction not available', Code.NotFound);
}

const { txp: perspective, txv: view } = await generateTransactionInfo(
const { txp: perspective, txv } = await generateTransactionInfo(
await fvk(),
transaction,
indexedDb.constants(),
);

// Invoke a higher-level translator on the transaction view.
const view = txvTranslator(txv);

const txInfo = new TransactionInfo({ height, id: req.id, transaction, perspective, view });
return { txInfo };
};
13 changes: 13 additions & 0 deletions packages/services/src/view-service/transparent-address.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { Impl } from './index.js';
import { fvkCtx } from '../ctx/full-viewing-key.js';
import { getTransparentAddress } from '@penumbra-zone/wasm/keys';

export const transparentAddress: Impl['transparentAddress'] = async (_, ctx) => {
const fvk = await ctx.values.get(fvkCtx)();
const t_addr = getTransparentAddress(fvk);

return {
address: t_addr.address,
encoding: t_addr.encoding,
};
};
32 changes: 32 additions & 0 deletions packages/services/src/view-service/util/transaction-view.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { TransactionView } from '@penumbra-zone/protobuf/penumbra/core/transaction/v1/transaction_pb';
import { getTransmissionKeyByAddress } from '@penumbra-zone/wasm/keys';

// Some transaction views (TXVs) require additional preprocessing before being rendered
// in the UI component library. For example, when handling IBC withdrawals with transparent
// addresses, this component transforms ephemeral addresses into their bech32-encoded
// transparent form to ensure the proper data is being displayed.
export const txvTranslator = (view: TransactionView): TransactionView => {
// 'Ics20Withdrawal' action view
if (!view.bodyView) {
return view;
}

const withdrawalAction = view.bodyView.actionViews.find(
action => action.actionView.case === 'ics20Withdrawal',
);

if (withdrawalAction?.actionView.case === 'ics20Withdrawal') {
const withdrawal = withdrawalAction.actionView.value;
// Create 80-byte array initialized to zeros, then set first 32 bytes to transmission key.
// This constructs a valid address format where:
// - First 32 bytes: transmission key
// - Remaining 48 bytes: zeroed (16-byte diversifier + 32-byte clue key)
if (withdrawal.returnAddress && withdrawal.useTransparentAddress) {
const newInner = new Uint8Array(80).fill(0);
newInner.set(getTransmissionKeyByAddress(withdrawal.returnAddress), 0);
withdrawal.returnAddress.inner = newInner;
}
}

return view;
};
Loading

0 comments on commit 95d5fd9

Please sign in to comment.