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
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
@@ -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;

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
@@ -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",
43 changes: 40 additions & 3 deletions v4-client-js/src/clients/composite-client.ts
Original file line number Diff line number Diff line change
@@ -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,
@@ -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,
@@ -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;
@@ -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,
@@ -160,6 +168,8 @@ export class CompositeClient {
memo,
broadcastMode,
account,
undefined,
authenticators,
);
}

@@ -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,
@@ -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(
@@ -329,6 +346,7 @@ export class CompositeClient {
memo,
undefined,
() => account,
permissionedKeysAccountAuth?.authenticators,
);
}

@@ -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
@@ -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';
@@ -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',
@@ -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',
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,
@@ -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,
]);
39 changes: 39 additions & 0 deletions v4-client-js/src/clients/modules/composer.ts
Original file line number Diff line number Diff line change
@@ -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,9 @@ 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,
AuthenticatorType,
} from '../constants';
import { DenomConfig } from '../types';
import {
@@ -105,6 +109,7 @@ export class Composer {
orderFlags,
clobPairId,
};

const order: Order = {
orderId,
side,
@@ -575,6 +580,40 @@ export class Composer {
}
}

// ----------- x/accountplus --------

public composeMsgAddAuthenticator(
address: string,
authenticatorType: AuthenticatorType,
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,
16 changes: 16 additions & 0 deletions v4-client-js/src/clients/modules/get.ts
Original file line number Diff line number Diff line change
@@ -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<GetAuthenticatorsResponse> {
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<Uint8Array> {
// eslint-disable-next-line max-len
const resp: QueryAbciResponse = await this.stargateQueryClient.queryAbci(
12 changes: 6 additions & 6 deletions v4-client-js/src/clients/modules/local-wallet.ts
Original file line number Diff line number Diff line change
@@ -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<LocalWallet> {
static async fromOfflineSigner(signer: OfflineDirectSigner): Promise<LocalWallet> {
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<void> {
async setSigner(signer: OfflineDirectSigner): Promise<void> {
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<void> {
@@ -60,6 +60,6 @@ export default class LocalWallet {
fee?: StdFee,
memo: string = '',
): Promise<Uint8Array> {
return this.signer!.signTransaction(messages, transactionOptions, fee, memo);
return this.signer!.signTransaction(messages, transactionOptions, fee, memo, this.pubKey);
}
}
45 changes: 44 additions & 1 deletion v4-client-js/src/clients/modules/post.ts
Original file line number Diff line number Diff line change
@@ -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';
@@ -178,6 +178,7 @@ export class Post {
broadcastMode?: BroadcastMode,
account?: () => Promise<Account>,
gasAdjustment: number = GAS_MULTIPLIER,
authenticators?: Long[],
): Promise<BroadcastTxAsyncResponse | BroadcastTxSyncResponse | IndexedTx> {
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<Uint8Array> {
// 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<BroadcastTxAsyncResponse | BroadcastTxSyncResponse | IndexedTx> {
const signedTransaction = await this.signTransaction(
wallet,
@@ -306,6 +311,7 @@ export class Post {
gasPrice,
memo,
gasAdjustment,
authenticators,
);
return this.sendSignedTransaction(signedTransaction, broadcastMode);
}
@@ -944,4 +950,41 @@ export class Post {
gasAdjustment,
);
}

async addAuthenticator(
subaccount: SubaccountInfo,
authenticatorType: AuthenticatorType,
data: Uint8Array,
): Promise<BroadcastTxAsyncResponse | BroadcastTxSyncResponse | IndexedTx> {
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<BroadcastTxAsyncResponse | BroadcastTxSyncResponse | IndexedTx> {
const msg = this.composer.composeMsgRemoveAuthenticator(subaccount.address, id);

return this.send(
subaccount.wallet,
() => Promise.resolve([msg]),
false,
undefined,
undefined,
Method.BroadcastTxSync,
undefined,
undefined,
);
}
}
92 changes: 79 additions & 13 deletions v4-client-js/src/clients/modules/signer.ts
Original file line number Diff line number Diff line change
@@ -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<Uint8Array> {
// 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());
}
}
1 change: 1 addition & 0 deletions v4-client-js/src/clients/types.ts
Original file line number Diff line number Diff line change
@@ -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