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

feat: Add Authenticator flow for Permissioned Keys #317

Merged
merged 7 commits into from
Jan 29, 2025
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
2 changes: 2 additions & 0 deletions v4-client-js/examples/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export const DYDX_LOCAL_ADDRESS = 'dydx199tqg4wdlnu4qjlxchpd7seg454937hjrknju4';
export const DYDX_LOCAL_MNEMONIC =
'merge panther lobster crazy road hollow amused security before critic about cliff exhibit cause coyote talent happy where lion river tobacco option coconut small';

export const DYDX_TEST_MNEMONIC_2 = 'movie yard still copper exile wear brisk chest ride dizzy novel future menu finish radar lunar claim hub middle force turtle mouse frequent embark';

export const MARKET_BTC_USD: string = 'BTC-USD';
export const PERPETUAL_PAIR_BTC_USD: number = 0;

Expand Down
110 changes: 110 additions & 0 deletions v4-client-js/examples/permissioned_keys_example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { TextEncoder } from 'util';

import { toBase64 } from '@cosmjs/encoding';
import { Order_TimeInForce } from '@dydxprotocol/v4-proto/src/codegen/dydxprotocol/clob/order';

import { BECH32_PREFIX } from '../src';
import { CompositeClient } from '../src/clients/composite-client';
import { AuthenticatorType, Network, OrderSide, SelectedGasDenom } from '../src/clients/constants';
import LocalWallet from '../src/clients/modules/local-wallet';
import { SubaccountInfo } from '../src/clients/subaccount';
import { DYDX_TEST_MNEMONIC, DYDX_TEST_MNEMONIC_2 } from './constants';

async function test(): Promise<void> {
const wallet1 = await LocalWallet.fromMnemonic(DYDX_TEST_MNEMONIC, BECH32_PREFIX);
const wallet2 = await LocalWallet.fromMnemonic(DYDX_TEST_MNEMONIC_2, BECH32_PREFIX);

const network = Network.staging();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be set to testnet?

const client = await CompositeClient.connect(network);
client.setSelectedGasDenom(SelectedGasDenom.NATIVE);

console.log('**Client**');
console.log(client);

const subaccount1 = new SubaccountInfo(wallet1, 0);
const subaccount2 = new SubaccountInfo(wallet2, 0);

// Change second wallet pubkey
// Add an authenticator to allow wallet2 to place orders
console.log("** Adding authenticator **");
await addAuthenticator(client, subaccount1, wallet2.pubKey!.value);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you add some delay here, as it seems it goes to fast between adding and doing the getAuthenticators step after that

await sleep(500);


const authenticators = await client.getAuthenticators(wallet1.address!);
// Last element in authenticators array is the most recently created
const lastElement = authenticators.accountAuthenticators.length - 1;
const authenticatorID = authenticators.accountAuthenticators[lastElement].id;

// Placing order using subaccount2 for subaccount1 succeeds
console.log("** Placing order with authenticator **");
await placeOrder(client, subaccount2, subaccount1, authenticatorID);

// Remove authenticator
console.log("** Removing authenticator **");
await removeAuthenticator(client, subaccount1, authenticatorID);

// Placing an order using subaccount2 will now fail
console.log("** Placing order with invalid authenticator should fail **");
await placeOrder(client, subaccount2, subaccount1, authenticatorID);
}

async function removeAuthenticator(client: CompositeClient,subaccount: SubaccountInfo, id: Long): Promise<void> {
await client.removeAuthenticator(subaccount, id);
}

async function addAuthenticator(client: CompositeClient, subaccount: SubaccountInfo, authedPubKey:string): Promise<void> {
const subAuthenticators = [{
type: AuthenticatorType.SIGNATURE_VERIFICATION,
config: authedPubKey,
},
{
type: AuthenticatorType.MESSAGE_FILTER,
config: toBase64(new TextEncoder().encode("/dydxprotocol.clob.MsgPlaceOrder")),
},
];

const jsonString = JSON.stringify(subAuthenticators);
const encodedData = new TextEncoder().encode(jsonString);

await client.addAuthenticator(subaccount, AuthenticatorType.ALL_OF, encodedData);
}

async function placeOrder(client: CompositeClient, fromAccount: SubaccountInfo, forAccount: SubaccountInfo, authenticatorId: Long): Promise<void> {
try {
const side = OrderSide.BUY
const price = Number("1000");
const currentBlock = await client.validatorClient.get.latestBlockHeight();
const nextValidBlockHeight = currentBlock + 5;
const goodTilBlock = nextValidBlockHeight + 10;

const timeInForce = Order_TimeInForce.TIME_IN_FORCE_UNSPECIFIED;

const clientId = Math.floor(Math.random() * 10000);

const tx = await client.placeShortTermOrder(
fromAccount,
'ETH-USD',
side,
price,
0.01,
clientId,
goodTilBlock,
timeInForce,
false,
undefined,
{
authenticators: [authenticatorId],
accountForOrder: forAccount,
}
);
console.log('**Order Tx**');
console.log(Buffer.from(tx.hash).toString('hex'));
} catch (error) {
console.log(error.message);
}
}

test()
.then(() => {})
.catch((error) => {
console.log(error.message);
});
23 changes: 10 additions & 13 deletions v4-client-js/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion v4-client-js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@
"@cosmjs/stargate": "^0.32.1",
"@cosmjs/tendermint-rpc": "^0.32.1",
"@cosmjs/utils": "^0.32.1",
"@dydxprotocol/v4-proto": "8.0.0",
"@osmonauts/lcd": "^0.6.0",
"@dydxprotocol/v4-proto": "7.0.0-dev.0",
"@scure/bip32": "^1.1.5",
"@scure/bip39": "^1.1.1",
"axios": "1.1.3",
Expand Down
43 changes: 40 additions & 3 deletions v4-client-js/src/clients/composite-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
BroadcastTxAsyncResponse,
BroadcastTxSyncResponse,
} from '@cosmjs/tendermint-rpc/build/tendermint37';
import { GetAuthenticatorsResponse } from '@dydxprotocol/v4-proto/src/codegen/dydxprotocol/accountplus/query';
import {
Order_ConditionType,
Order_TimeInForce,
Expand All @@ -17,6 +18,7 @@ import { bigIntToBytes } from '../lib/helpers';
import { isStatefulOrder, verifyOrderFlags } from '../lib/validation';
import { GovAddNewMarketParams, OrderFlags } from '../types';
import {
AuthenticatorType,
Network,
OrderExecution,
OrderSide,
Expand Down Expand Up @@ -66,6 +68,11 @@ export interface OrderBatchWithMarketId {
clientIds: number[];
}

export interface PermissionedKeysAccountAuth {
authenticators: Long[];
accountForOrder: SubaccountInfo;
}

export class CompositeClient {
public readonly network: Network;
public gasDenom: SelectedGasDenom = SelectedGasDenom.USDC;
Expand Down Expand Up @@ -151,6 +158,7 @@ export class CompositeClient {
memo?: string,
broadcastMode?: BroadcastMode,
account?: () => Promise<Account>,
authenticators?: Long[],
): Promise<BroadcastTxAsyncResponse | BroadcastTxSyncResponse | IndexedTx> {
return this.validatorClient.post.send(
wallet,
Expand All @@ -160,6 +168,8 @@ export class CompositeClient {
memo,
broadcastMode,
account,
undefined,
authenticators,
);
}

Expand Down Expand Up @@ -297,10 +307,15 @@ export class CompositeClient {
timeInForce: Order_TimeInForce,
reduceOnly: boolean,
memo?: string,
permissionedKeysAccountAuth?: PermissionedKeysAccountAuth,
): Promise<BroadcastTxAsyncResponse | BroadcastTxSyncResponse | IndexedTx> {
// For permissioned orders, use the permissioning account details instead of the subaccount
// This allows placing orders on behalf of another account when using permissioned keys
const accountForOrder = permissionedKeysAccountAuth ? permissionedKeysAccountAuth.accountForOrder : subaccount;
const msgs: Promise<EncodeObject[]> = new Promise((resolve, reject) => {

const msg = this.placeShortTermOrderMessage(
subaccount,
accountForOrder,
marketId,
side,
price,
Expand All @@ -311,14 +326,16 @@ export class CompositeClient {
reduceOnly,
);
msg
.then((it) => resolve([it]))
.then((it) => {
resolve([it]);
})
.catch((err) => {
console.log(err);
reject(err);
});
});
const account: Promise<Account> = this.validatorClient.post.account(
subaccount.address,
accountForOrder.address,
undefined,
);
return this.send(
Expand All @@ -329,6 +346,7 @@ export class CompositeClient {
memo,
undefined,
() => account,
permissionedKeysAccountAuth?.authenticators,
);
}

Expand Down Expand Up @@ -1217,4 +1235,23 @@ export class CompositeClient {
): Promise<BroadcastTxAsyncResponse | BroadcastTxSyncResponse | IndexedTx> {
return this.validatorClient.post.createMarketPermissionless(ticker, subaccount, broadcastMode, gasAdjustment, memo);
}

async addAuthenticator(
subaccount: SubaccountInfo,
authenticatorType: AuthenticatorType,
data: Uint8Array,
): Promise<BroadcastTxAsyncResponse | BroadcastTxSyncResponse | IndexedTx> {
return this.validatorClient.post.addAuthenticator(subaccount, authenticatorType, data)
}

async removeAuthenticator(
subaccount: SubaccountInfo,
id: Long,
): Promise<BroadcastTxAsyncResponse | BroadcastTxSyncResponse | IndexedTx> {
return this.validatorClient.post.removeAuthenticator(subaccount, id)
}

async getAuthenticators(address: string): Promise<GetAuthenticatorsResponse>{
return this.validatorClient.get.getAuthenticators(address);
}
}
17 changes: 16 additions & 1 deletion v4-client-js/src/clients/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ export const TYPE_URL_MSG_UNDELEGATE = '/cosmos.staking.v1beta1.MsgUndelegate';
export const TYPE_URL_MSG_WITHDRAW_DELEGATOR_REWARD =
'/cosmos.distribution.v1beta1.MsgWithdrawDelegatorReward';

// x/accountplus
export const TYPE_URL_MSG_ADD_AUTHENTICATOR = '/dydxprotocol.accountplus.MsgAddAuthenticator'
export const TYPE_URL_MSG_REMOVE_AUTHENTICATOR = '/dydxprotocol.accountplus.MsgRemoveAuthenticator'

// ------------ Chain Constants ------------
// The following are same across different networks / deployments.
export const GOV_MODULE_ADDRESS = 'dydx10d07y265gmmuvt4z0w9aw880jnsr700jnmapky';
Expand Down Expand Up @@ -196,6 +200,17 @@ export enum PnlTickInterval {
day = 'day',
}

// ----------- Authenticators -------------

export enum AuthenticatorType {
ALL_OF = 'AllOf',
ANY_OF = 'AnyOf',
SIGNATURE_VERIFICATION = 'SignatureVerification',
MESSAGE_FILTER = 'MessageFilter',
CLOB_PAIR_ID_FILTER = 'ClobPairIdFilter',
SUBACCOUNT_FILTER = 'SubaccountFilter',
}

export enum TradingRewardAggregationPeriod {
DAILY = 'DAILY',
WEEKLY = 'WEEKLY',
Expand Down Expand Up @@ -285,7 +300,7 @@ export class Network {
const indexerConfig = new IndexerConfig(IndexerApiHost.STAGING, IndexerWSHost.STAGING);
const validatorConfig = new ValidatorConfig(
ValidatorApiHost.STAGING,
TESTNET_CHAIN_ID,
STAGING_CHAIN_ID,
{
CHAINTOKEN_DENOM: 'adv4tnt',
USDC_DENOM: 'ibc/8E27BA2D5493AF5636760E354E46004562C46AB7EC0CC4C1CA14E9E20E2545B5',
Expand Down
8 changes: 8 additions & 0 deletions v4-client-js/src/clients/lib/registry.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { GeneratedType, Registry } from '@cosmjs/proto-signing';
import { defaultRegistryTypes } from '@cosmjs/stargate';
import { MsgAddAuthenticator, MsgRemoveAuthenticator } from '@dydxprotocol/v4-proto/src/codegen/dydxprotocol/accountplus/tx';
import { MsgRegisterAffiliate } from '@dydxprotocol/v4-proto/src/codegen/dydxprotocol/affiliates/tx';
import {
MsgPlaceOrder,
Expand Down Expand Up @@ -38,6 +39,8 @@ import {
TYPE_URL_MSG_WITHDRAW_FROM_MEGAVAULT,
TYPE_URL_MSG_REGISTER_AFFILIATE,
TYPE_URL_MSG_CREATE_MARKET_PERMISSIONLESS,
TYPE_URL_MSG_ADD_AUTHENTICATOR,
TYPE_URL_MSG_REMOVE_AUTHENTICATOR,
} from '../constants';

export const registry: ReadonlyArray<[string, GeneratedType]> = [];
Expand Down Expand Up @@ -74,6 +77,11 @@ export function generateRegistry(): Registry {
// affiliates
[TYPE_URL_MSG_REGISTER_AFFILIATE, MsgRegisterAffiliate as GeneratedType],


// authentication
[TYPE_URL_MSG_ADD_AUTHENTICATOR, MsgAddAuthenticator as GeneratedType],
[TYPE_URL_MSG_REMOVE_AUTHENTICATOR, MsgRemoveAuthenticator as GeneratedType],

// default types
...defaultRegistryTypes,
]);
Expand Down
Loading
Loading