Skip to content
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

ibc: transparent address support #1950

Merged
merged 20 commits into from
Dec 22, 2024
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
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,
TalDerei marked this conversation as resolved.
Show resolved Hide resolved
},
],
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: {
TalDerei marked this conversation as resolved.
Show resolved Hide resolved
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