From e3d715fe01825c2f4665c70d064b9ea3b7e67fb1 Mon Sep 17 00:00:00 2001 From: Adam Fraser Date: Tue, 3 Dec 2024 13:26:27 -0500 Subject: [PATCH 1/6] Add add and remove authenticator functions --- v4-client-js/src/clients/composite-client.ts | 16 ++++++++ v4-client-js/src/clients/constants.ts | 18 ++++++++- v4-client-js/src/clients/lib/registry.ts | 8 ++++ v4-client-js/src/clients/modules/composer.ts | 37 +++++++++++++++++++ v4-client-js/src/clients/modules/post.ts | 39 +++++++++++++++++++- 5 files changed, 116 insertions(+), 2 deletions(-) diff --git a/v4-client-js/src/clients/composite-client.ts b/v4-client-js/src/clients/composite-client.ts index 02fd3783..ead82dd6 100644 --- a/v4-client-js/src/clients/composite-client.ts +++ b/v4-client-js/src/clients/composite-client.ts @@ -17,6 +17,7 @@ import { bigIntToBytes } from '../lib/helpers'; import { isStatefulOrder, verifyOrderFlags } from '../lib/validation'; import { GovAddNewMarketParams, OrderFlags } from '../types'; import { + AuthenticatorType, Network, OrderExecution, OrderSide, @@ -1217,4 +1218,19 @@ export class CompositeClient { ): Promise { return this.validatorClient.post.createMarketPermissionless(ticker, subaccount, broadcastMode, gasAdjustment, memo); } + + async addAuthenticator( + subaccount: SubaccountInfo, + authenticatorType: string, + data: Uint8Array, + ): Promise { + return this.validatorClient.post.addAuthenticator(subaccount, authenticatorType, data) + } + + async removeAuthenticator( + subaccount: SubaccountInfo, + id: Long, + ): Promise { + return this.validatorClient.post.removeAuthenticator(subaccount, id) + } } diff --git a/v4-client-js/src/clients/constants.ts b/v4-client-js/src/clients/constants.ts index 694ba123..ed0b16e3 100644 --- a/v4-client-js/src/clients/constants.ts +++ b/v4-client-js/src/clients/constants.ts @@ -115,6 +115,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'; @@ -195,6 +199,18 @@ 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', +} + + // ------------ API Defaults ------------ export const DEFAULT_API_TIMEOUT: number = 3_000; @@ -278,7 +294,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', diff --git a/v4-client-js/src/clients/lib/registry.ts b/v4-client-js/src/clients/lib/registry.ts index 8a93b870..080d2a1a 100644 --- a/v4-client-js/src/clients/lib/registry.ts +++ b/v4-client-js/src/clients/lib/registry.ts @@ -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, @@ -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]> = []; @@ -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, ]); diff --git a/v4-client-js/src/clients/modules/composer.ts b/v4-client-js/src/clients/modules/composer.ts index 7c1fee8c..bdfe03b1 100644 --- a/v4-client-js/src/clients/modules/composer.ts +++ b/v4-client-js/src/clients/modules/composer.ts @@ -7,6 +7,7 @@ import { MsgDelegate, MsgUndelegate, } from '@dydxprotocol/v4-proto/src/codegen/cosmos/staking/v1beta1/tx'; +import { MsgAddAuthenticator, MsgRemoveAuthenticator } from '@dydxprotocol/v4-proto/src/codegen/dydxprotocol/accountplus/tx'; import { MsgRegisterAffiliate } from '@dydxprotocol/v4-proto/src/codegen/dydxprotocol/affiliates/tx'; import { ClobPair_Status } from '@dydxprotocol/v4-proto/src/codegen/dydxprotocol/clob/clob_pair'; import { @@ -52,6 +53,8 @@ import { TYPE_URL_MSG_DEPOSIT_TO_MEGAVAULT, TYPE_URL_MSG_WITHDRAW_FROM_MEGAVAULT, TYPE_URL_MSG_CREATE_MARKET_PERMISSIONLESS, + TYPE_URL_MSG_ADD_AUTHENTICATOR, + TYPE_URL_MSG_REMOVE_AUTHENTICATOR, } from '../constants'; import { DenomConfig } from '../types'; import { @@ -575,6 +578,40 @@ export class Composer { } } + // ----------- x/accountplus -------- + + public composeMsgAddAuthenticator( + address: string, + authenticatorType: string, + data: Uint8Array, + ): EncodeObject { + const msg: MsgAddAuthenticator = { + sender: address, + authenticatorType, + data, + } + + return { + typeUrl: TYPE_URL_MSG_ADD_AUTHENTICATOR, + value: msg, + } + } + + public composeMsgRemoveAuthenticator( + address: string, + id: Long, + ): EncodeObject { + const msg: MsgRemoveAuthenticator = { + sender: address, + id, + } + + return { + typeUrl: TYPE_URL_MSG_REMOVE_AUTHENTICATOR, + value: msg, + } + } + // ------------ util ------------ public validateGoodTilBlockAndTime( orderFlags: number, diff --git a/v4-client-js/src/clients/modules/post.ts b/v4-client-js/src/clients/modules/post.ts index af47fa4d..29a9c6f4 100644 --- a/v4-client-js/src/clients/modules/post.ts +++ b/v4-client-js/src/clients/modules/post.ts @@ -11,7 +11,7 @@ import _ from 'lodash'; import Long from 'long'; import protobuf from 'protobufjs'; -import { GAS_MULTIPLIER, SelectedGasDenom } from '../constants'; +import { AuthenticatorType, GAS_MULTIPLIER, SelectedGasDenom } from '../constants'; import { UnexpectedClientError } from '../lib/errors'; import { generateRegistry } from '../lib/registry'; import { SubaccountInfo } from '../subaccount'; @@ -944,4 +944,41 @@ export class Post { gasAdjustment, ); } + + async addAuthenticator( + subaccount: SubaccountInfo, + authenticatorType: AuthenticatorType, + data: Uint8Array, + ): Promise { + const msg = this.composer.composeMsgAddAuthenticator(subaccount.address, authenticatorType, data); + + return this.send( + subaccount.wallet, + () => Promise.resolve([msg]), + false, + undefined, + undefined, + Method.BroadcastTxSync, + undefined, + undefined, + ); + } + + async removeAuthenticator( + subaccount: SubaccountInfo, + id: Long, + ): Promise { + const msg = this.composer.composeMsgRemoveAuthenticator(subaccount.address, id); + + return this.send( + subaccount.wallet, + () => Promise.resolve([msg]), + false, + undefined, + undefined, + Method.BroadcastTxSync, + undefined, + undefined, + ); + } } From 08826960328ee19f7d1e92a38495736ca552b055 Mon Sep 17 00:00:00 2001 From: Adam Fraser Date: Thu, 23 Jan 2025 15:02:02 -0500 Subject: [PATCH 2/6] Add functionality to sign messages using authenticator flow --- v4-client-js/package-lock.json | 15 ++- v4-client-js/package.json | 2 +- v4-client-js/src/clients/composite-client.ts | 24 ++++- v4-client-js/src/clients/modules/composer.ts | 4 +- .../src/clients/modules/local-wallet.ts | 12 +-- v4-client-js/src/clients/modules/post.ts | 6 ++ v4-client-js/src/clients/modules/signer.ts | 92 ++++++++++++++++--- v4-client-js/src/clients/types.ts | 1 + 8 files changed, 122 insertions(+), 34 deletions(-) diff --git a/v4-client-js/package-lock.json b/v4-client-js/package-lock.json index 17b9e2ce..eb327f25 100644 --- a/v4-client-js/package-lock.json +++ b/v4-client-js/package-lock.json @@ -2852,15 +2852,12 @@ } }, "node_modules/@scure/base": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz", - "integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ] + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", + "funding": { + "url": "https://paulmillr.com/funding/" + } }, "node_modules/@scure/bip32": { "version": "1.3.0", diff --git a/v4-client-js/package.json b/v4-client-js/package.json index efacf1e4..ccbeb131 100644 --- a/v4-client-js/package.json +++ b/v4-client-js/package.json @@ -38,8 +38,8 @@ "@cosmjs/stargate": "^0.32.1", "@cosmjs/tendermint-rpc": "^0.32.1", "@cosmjs/utils": "^0.32.1", - "@osmonauts/lcd": "^0.6.0", "@dydxprotocol/v4-proto": "7.0.0-dev.0", + "@osmonauts/lcd": "^0.6.0", "@scure/bip32": "^1.1.5", "@scure/bip39": "^1.1.1", "axios": "1.1.3", diff --git a/v4-client-js/src/clients/composite-client.ts b/v4-client-js/src/clients/composite-client.ts index ead82dd6..c9ae94cc 100644 --- a/v4-client-js/src/clients/composite-client.ts +++ b/v4-client-js/src/clients/composite-client.ts @@ -67,6 +67,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; @@ -152,6 +157,7 @@ export class CompositeClient { memo?: string, broadcastMode?: BroadcastMode, account?: () => Promise, + authenticators?: Long[], ): Promise { return this.validatorClient.post.send( wallet, @@ -161,6 +167,8 @@ export class CompositeClient { memo, broadcastMode, account, + undefined, + authenticators, ); } @@ -298,10 +306,15 @@ export class CompositeClient { timeInForce: Order_TimeInForce, reduceOnly: boolean, memo?: string, + permissionedKeysAccountAuth?: PermissionedKeysAccountAuth, ): Promise { + // 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 = new Promise((resolve, reject) => { + const msg = this.placeShortTermOrderMessage( - subaccount, + accountForOrder, marketId, side, price, @@ -312,14 +325,16 @@ export class CompositeClient { reduceOnly, ); msg - .then((it) => resolve([it])) + .then((it) => { + resolve([it]); + }) .catch((err) => { console.log(err); reject(err); }); }); const account: Promise = this.validatorClient.post.account( - subaccount.address, + accountForOrder.address, undefined, ); return this.send( @@ -330,6 +345,7 @@ export class CompositeClient { memo, undefined, () => account, + permissionedKeysAccountAuth?.authenticators, ); } @@ -1221,7 +1237,7 @@ export class CompositeClient { async addAuthenticator( subaccount: SubaccountInfo, - authenticatorType: string, + authenticatorType: AuthenticatorType, data: Uint8Array, ): Promise { return this.validatorClient.post.addAuthenticator(subaccount, authenticatorType, data) diff --git a/v4-client-js/src/clients/modules/composer.ts b/v4-client-js/src/clients/modules/composer.ts index bdfe03b1..edc16f18 100644 --- a/v4-client-js/src/clients/modules/composer.ts +++ b/v4-client-js/src/clients/modules/composer.ts @@ -55,6 +55,7 @@ import { TYPE_URL_MSG_CREATE_MARKET_PERMISSIONLESS, TYPE_URL_MSG_ADD_AUTHENTICATOR, TYPE_URL_MSG_REMOVE_AUTHENTICATOR, + AuthenticatorType, } from '../constants'; import { DenomConfig } from '../types'; import { @@ -108,6 +109,7 @@ export class Composer { orderFlags, clobPairId, }; + const order: Order = { orderId, side, @@ -582,7 +584,7 @@ export class Composer { public composeMsgAddAuthenticator( address: string, - authenticatorType: string, + authenticatorType: AuthenticatorType, data: Uint8Array, ): EncodeObject { const msg: MsgAddAuthenticator = { diff --git a/v4-client-js/src/clients/modules/local-wallet.ts b/v4-client-js/src/clients/modules/local-wallet.ts index 22e38b2d..bf17cb22 100644 --- a/v4-client-js/src/clients/modules/local-wallet.ts +++ b/v4-client-js/src/clients/modules/local-wallet.ts @@ -3,7 +3,7 @@ import { AccountData, DirectSecp256k1HdWallet, EncodeObject, - OfflineSigner, + OfflineDirectSigner, } from '@cosmjs/proto-signing'; import { SigningStargateClient } from '@cosmjs/stargate'; import Long from 'long'; @@ -22,9 +22,9 @@ export default class LocalWallet { address?: string; pubKey?: Secp256k1Pubkey; signer?: TransactionSigner; - offlineSigner?: OfflineSigner; + offlineSigner?: OfflineDirectSigner; - static async fromOfflineSigner(signer: OfflineSigner): Promise { + static async fromOfflineSigner(signer: OfflineDirectSigner): Promise { const wallet = new LocalWallet(); await wallet.setSigner(signer); return wallet; @@ -36,7 +36,7 @@ export default class LocalWallet { return wallet; } - async setSigner(signer: OfflineSigner): Promise { + async setSigner(signer: OfflineDirectSigner): Promise { this.offlineSigner = signer; const stargateClient = await SigningStargateClient.offline(signer, { registry: generateRegistry(), @@ -46,7 +46,7 @@ export default class LocalWallet { this.accounts = [...accountData]; this.address = firstAccount.address; this.pubKey = encodeSecp256k1Pubkey(firstAccount.pubkey); - this.signer = new TransactionSigner(this.address, stargateClient); + this.signer = new TransactionSigner(this.address, stargateClient, signer); } async setMnemonic(mnemonic: string, prefix?: string): Promise { @@ -60,6 +60,6 @@ export default class LocalWallet { fee?: StdFee, memo: string = '', ): Promise { - return this.signer!.signTransaction(messages, transactionOptions, fee, memo); + return this.signer!.signTransaction(messages, transactionOptions, fee, memo, this.pubKey); } } diff --git a/v4-client-js/src/clients/modules/post.ts b/v4-client-js/src/clients/modules/post.ts index 29a9c6f4..23adc0ac 100644 --- a/v4-client-js/src/clients/modules/post.ts +++ b/v4-client-js/src/clients/modules/post.ts @@ -178,6 +178,7 @@ export class Post { broadcastMode?: BroadcastMode, account?: () => Promise, gasAdjustment: number = GAS_MULTIPLIER, + authenticators?: Long[], ): Promise { const msgsPromise = messaging(); const accountPromise = account ? await account() : this.account(wallet.address!); @@ -193,6 +194,7 @@ export class Post { memo ?? this.defaultClientMemo, broadcastMode ?? this.defaultBroadcastMode(msgs), gasAdjustment, + authenticators, ); } @@ -238,6 +240,7 @@ export class Post { gasPrice: GasPrice = this.getGasPrice(), memo?: string, gasAdjustment: number = GAS_MULTIPLIER, + authenticators?: Long[], ): Promise { // protocol expects timestamp nonce in UTC milliseconds, which is the unit returned by Date.now() const sequence = this.useTimestampNonce ? Date.now() : account.sequence; @@ -260,6 +263,7 @@ export class Post { sequence, accountNumber: account.accountNumber, chainId: this.chainId, + authenticators, }; // Generate signed transaction. return wallet.signTransaction(messages, txOptions, fee, memo); @@ -297,6 +301,7 @@ export class Post { memo?: string, broadcastMode?: BroadcastMode, gasAdjustment: number = GAS_MULTIPLIER, + authenticators?: Long[], ): Promise { const signedTransaction = await this.signTransaction( wallet, @@ -306,6 +311,7 @@ export class Post { gasPrice, memo, gasAdjustment, + authenticators, ); return this.sendSignedTransaction(signedTransaction, broadcastMode); } diff --git a/v4-client-js/src/clients/modules/signer.ts b/v4-client-js/src/clients/modules/signer.ts index bc727158..54dc1ed6 100644 --- a/v4-client-js/src/clients/modules/signer.ts +++ b/v4-client-js/src/clients/modules/signer.ts @@ -1,10 +1,15 @@ -import { EncodeObject } from '@cosmjs/proto-signing'; +import { Secp256k1Pubkey } from '@cosmjs/amino'; +import { fromBase64 } from '@cosmjs/encoding'; +import { Int53 } from '@cosmjs/math'; +import { EncodeObject, encodePubkey, makeAuthInfoBytes, makeSignDoc, OfflineDirectSigner } from '@cosmjs/proto-signing'; import { SigningStargateClient, StdFee } from '@cosmjs/stargate'; -import { TxRaw } from 'cosmjs-types/cosmos/tx/v1beta1/tx'; +import { TxExtension } from '@dydxprotocol/v4-proto/src/codegen/dydxprotocol/accountplus/tx'; +import { TxBody, TxRaw } from 'cosmjs-types/cosmos/tx/v1beta1/tx'; +import { Any } from 'cosmjs-types/google/protobuf/any'; import Long from 'long'; import protobuf from 'protobufjs'; -import { UserError } from '../lib/errors'; +import { generateRegistry } from '../lib/registry'; import { TransactionOptions } from '../types'; // Required for encoding and decoding queries that are of type Long. @@ -17,10 +22,16 @@ protobuf.configure(); export class TransactionSigner { readonly address: string; readonly stargateSigningClient: SigningStargateClient; + readonly offlineSigner: OfflineDirectSigner; - constructor(address: string, stargateSigningClient: SigningStargateClient) { + constructor( + address: string, + stargateSigningClient: SigningStargateClient, + offlineSigner: OfflineDirectSigner, + ) { this.address = address; this.stargateSigningClient = stargateSigningClient; + this.offlineSigner = offlineSigner; } /** @@ -35,18 +46,73 @@ export class TransactionSigner { transactionOptions: TransactionOptions, fee?: StdFee, memo: string = '', + publicKey?: Secp256k1Pubkey, ): Promise { - // Verify there is either a fee or a path to getting the fee present. - if (fee === undefined) { - throw new UserError('fee cannot be undefined'); + if (!fee) { + throw new Error('Fee cannot be undefined'); } - // Sign, encode and return the transaction. - const rawTx: TxRaw = await this.stargateSigningClient.sign(this.address, messages, fee, memo, { - accountNumber: transactionOptions.accountNumber, - sequence: transactionOptions.sequence, - chainId: transactionOptions.chainId, + const registry = generateRegistry(); + + // Encode the messages + const encodedMessages = messages.map((msg) => registry.encodeAsAny(msg)); + + // Encode the TxExtension message + const txExtension = TxExtension.encode({ + selectedAuthenticators: transactionOptions.authenticators ?? [], + }).finish(); + + // Create the non-critical extension message + const nonCriticalExtensionOptions: Any[] = [ + Any.fromPartial({ + typeUrl: '/dydxprotocol.accountplus.TxExtension', + value: txExtension, + }), + ]; + + // Construct the TxBody + const txBody: TxBody = TxBody.fromPartial({ + messages: encodedMessages, + memo, + extensionOptions: [], + nonCriticalExtensionOptions, }); - return Uint8Array.from(TxRaw.encode(rawTx).finish()); + + // Encode the TxBody + const txBodyBytes = TxBody.encode(txBody).finish(); + + if (!publicKey) { + throw new Error('Public key cannot be undefined'); + } + const pubkey = encodePubkey(publicKey); // Use the public key of the signer + + const gasLimit = Int53.fromString(String(fee.gas)).toNumber(); + const authInfoBytes = makeAuthInfoBytes( + [{ pubkey, sequence: transactionOptions.sequence }], + fee.amount, + gasLimit, + undefined, + undefined, + ); + + // Create the SignDoc + const signDoc = makeSignDoc( + txBodyBytes, + authInfoBytes, + transactionOptions.chainId, + transactionOptions.accountNumber, + ); + + // Use OfflineSigner to sign the transaction + const signerAddress = this.address; + const { signed, signature } = await this.offlineSigner.signDirect(signerAddress, signDoc); + + const txRaw = TxRaw.fromPartial({ + bodyBytes: signed.bodyBytes, + authInfoBytes: signed.authInfoBytes, + signatures: [fromBase64(signature.signature)], + }); + + return Uint8Array.from(TxRaw.encode(txRaw).finish()); } } diff --git a/v4-client-js/src/clients/types.ts b/v4-client-js/src/clients/types.ts index 89c8cb5d..8e146a97 100644 --- a/v4-client-js/src/clients/types.ts +++ b/v4-client-js/src/clients/types.ts @@ -29,6 +29,7 @@ export interface PartialTransactionOptions { // Information for signing a transaction while offline. export interface TransactionOptions extends PartialTransactionOptions { sequence: number; + authenticators?: Long[]; } // OrderFlags, just a number in proto, defined as enum for convenience From 1318c94d5ee3aed756f0d7f7331ed698c2dfe04b Mon Sep 17 00:00:00 2001 From: Adam Fraser Date: Fri, 24 Jan 2025 14:37:28 -0500 Subject: [PATCH 3/6] Bump v4-proto to v8 --- v4-client-js/package-lock.json | 8 ++++---- v4-client-js/package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/v4-client-js/package-lock.json b/v4-client-js/package-lock.json index eb327f25..d1d0962d 100644 --- a/v4-client-js/package-lock.json +++ b/v4-client-js/package-lock.json @@ -16,7 +16,7 @@ "@cosmjs/stargate": "^0.32.1", "@cosmjs/tendermint-rpc": "^0.32.1", "@cosmjs/utils": "^0.32.1", - "@dydxprotocol/v4-proto": "7.0.0-dev.0", + "@dydxprotocol/v4-proto": "8.0.0", "@osmonauts/lcd": "^0.6.0", "@scure/bip32": "^1.1.5", "@scure/bip39": "^1.1.1", @@ -1930,9 +1930,9 @@ } }, "node_modules/@dydxprotocol/v4-proto": { - "version": "7.0.0-dev.0", - "resolved": "https://registry.npmjs.org/@dydxprotocol/v4-proto/-/v4-proto-7.0.0-dev.0.tgz", - "integrity": "sha512-yQ3xMW8GmKCCwtzXF1E/TMYvPYDPRmAR2T/AFXKlE2YF/P/yQMrz/IySzX4Z+wyAMI+G4Sr+AML7V8ehvAcjog==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@dydxprotocol/v4-proto/-/v4-proto-8.0.0.tgz", + "integrity": "sha512-QO4YLuptOH2BhkdKpXOvdSdGCy5IYUPYl/w4Xj3aoTrxCTa4Dp41HBC69ji8dzQ40eTLyCx5K+/+PCcBdZt4iw==", "dependencies": { "protobufjs": "^6.11.2" } diff --git a/v4-client-js/package.json b/v4-client-js/package.json index ccbeb131..a820c5ed 100644 --- a/v4-client-js/package.json +++ b/v4-client-js/package.json @@ -38,7 +38,7 @@ "@cosmjs/stargate": "^0.32.1", "@cosmjs/tendermint-rpc": "^0.32.1", "@cosmjs/utils": "^0.32.1", - "@dydxprotocol/v4-proto": "7.0.0-dev.0", + "@dydxprotocol/v4-proto": "8.0.0", "@osmonauts/lcd": "^0.6.0", "@scure/bip32": "^1.1.5", "@scure/bip39": "^1.1.1", From a1d3b9e0cfb9c92272679228cccd47d9002fd61e Mon Sep 17 00:00:00 2001 From: Adam Fraser Date: Fri, 24 Jan 2025 15:30:42 -0500 Subject: [PATCH 4/6] Add getAuthenticators helper function --- v4-client-js/src/clients/composite-client.ts | 5 +++++ v4-client-js/src/clients/modules/get.ts | 16 ++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/v4-client-js/src/clients/composite-client.ts b/v4-client-js/src/clients/composite-client.ts index c9ae94cc..3b95b842 100644 --- a/v4-client-js/src/clients/composite-client.ts +++ b/v4-client-js/src/clients/composite-client.ts @@ -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, @@ -1249,4 +1250,8 @@ export class CompositeClient { ): Promise { return this.validatorClient.post.removeAuthenticator(subaccount, id) } + + async getAuthenticators(address: string): Promise{ + return this.validatorClient.get.getAuthenticators(address); + } } diff --git a/v4-client-js/src/clients/modules/get.ts b/v4-client-js/src/clients/modules/get.ts index 04c15d9d..364f0e20 100644 --- a/v4-client-js/src/clients/modules/get.ts +++ b/v4-client-js/src/clients/modules/get.ts @@ -7,6 +7,7 @@ import { TxExtension, QueryAbciResponse, } from '@cosmjs/stargate'; +import { GetAuthenticatorsRequest, GetAuthenticatorsResponse } from '@dydxprotocol/v4-proto/src/codegen/dydxprotocol/accountplus/query'; import * as AuthModule from 'cosmjs-types/cosmos/auth/v1beta1/query'; import * as BankModule from 'cosmjs-types/cosmos/bank/v1beta1/query'; import { Any } from 'cosmjs-types/google/protobuf/any'; @@ -610,6 +611,21 @@ export class Get { return AffiliateModule.AffiliateWhitelistResponse.decode(data); } + async getAuthenticators(address: string): Promise { + const requestData = Uint8Array.from( + GetAuthenticatorsRequest.encode({ + account: address, + }).finish(), + ); + + const data = await this.sendQuery( + '/dydxprotocol.accountplus.Query/GetAuthenticators', + requestData, + ); + + return GetAuthenticatorsResponse.decode(data); + } + private async sendQuery(requestUrl: string, requestData: Uint8Array): Promise { // eslint-disable-next-line max-len const resp: QueryAbciResponse = await this.stargateQueryClient.queryAbci( From 93bb919c2ee5ad3e9389c4a56598777985ee3eff Mon Sep 17 00:00:00 2001 From: Adam Fraser Date: Fri, 24 Jan 2025 15:42:29 -0500 Subject: [PATCH 5/6] Add second test wallet mnemonic --- v4-client-js/examples/constants.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/v4-client-js/examples/constants.ts b/v4-client-js/examples/constants.ts index 2c9333ac..6eb5c989 100644 --- a/v4-client-js/examples/constants.ts +++ b/v4-client-js/examples/constants.ts @@ -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; From ca2c8978b095d296f9a728a4811d7cdd90e62248 Mon Sep 17 00:00:00 2001 From: Adam Fraser Date: Fri, 24 Jan 2025 15:42:51 -0500 Subject: [PATCH 6/6] feat: add Authenticator flow for Permissioned Keys --- .../examples/permissioned_keys_example.ts | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 v4-client-js/examples/permissioned_keys_example.ts diff --git a/v4-client-js/examples/permissioned_keys_example.ts b/v4-client-js/examples/permissioned_keys_example.ts new file mode 100644 index 00000000..d2d64e06 --- /dev/null +++ b/v4-client-js/examples/permissioned_keys_example.ts @@ -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 { + 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(); + 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); + + 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 { + await client.removeAuthenticator(subaccount, id); +} + +async function addAuthenticator(client: CompositeClient, subaccount: SubaccountInfo, authedPubKey:string): Promise { + 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 { + 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); + });