Skip to content

Commit 1d2bd29

Browse files
feat(sdk-coin-sui): sui sponsorship support
TICKET: WIN-6028
1 parent aeb595f commit 1d2bd29

File tree

6 files changed

+119
-17
lines changed

6 files changed

+119
-17
lines changed

modules/sdk-coin-sui/src/lib/iface.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,20 @@ import {
33
TransactionType as BitGoTransactionType,
44
} from '@bitgo/sdk-core';
55
import BigNumber from 'bignumber.js';
6-
import {
7-
CallArg,
8-
GasData,
9-
ProgrammableTransaction,
10-
SuiAddress,
11-
SuiObjectRef,
12-
TransactionExpiration,
13-
} from './mystenlab/types';
6+
import { CallArg, ProgrammableTransaction, SuiAddress, SuiObjectRef, TransactionExpiration } from './mystenlab/types';
7+
8+
export interface GasData {
9+
payment: SuiObjectRef[];
10+
owner: SuiAddress;
11+
price: number;
12+
budget: number;
13+
sponsor?: SuiAddress;
14+
}
15+
16+
export interface GasDataWithSponsor extends GasData {
17+
sponsor: SuiAddress;
18+
}
19+
1420
import { TransactionBlockInput, TransactionType } from './mystenlab/builder';
1521

1622
export enum SuiTransactionType {

modules/sdk-coin-sui/src/lib/mystenlab/builder/TransactionDataBlock.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ const GasConfig = object({
4343
price: optional(StringEncodedBigint),
4444
payment: optional(array(SuiObjectRef)),
4545
owner: optional(SuiAddress),
46+
sponsor: optional(SuiAddress),
4647
});
4748
type GasConfig = Infer<typeof GasConfig>;
4849

@@ -213,6 +214,9 @@ export class TransactionBlockDataBuilder {
213214
owner: prepareSuiAddress(this.gasConfig.owner ?? sender),
214215
price: BigInt(gasConfig.price),
215216
budget: BigInt(gasConfig.budget),
217+
...(gasConfig.sponsor && {
218+
sponsor: prepareSuiAddress(gasConfig.sponsor),
219+
}),
216220
},
217221
kind: {
218222
ProgrammableTransaction: {

modules/sdk-coin-sui/src/lib/transaction.ts

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@ import {
66
Signature,
77
TransactionType as BitGoTransactionType,
88
} from '@bitgo/sdk-core';
9-
import { SuiProgrammableTransaction, SuiTransaction, SuiTransactionType, TxData } from './iface';
9+
import { SuiProgrammableTransaction, SuiTransaction, SuiTransactionType, TxData, GasData } from './iface';
1010
import { BaseCoin as CoinConfig } from '@bitgo/statics';
1111
import utils, { AppId, Intent, IntentScope, IntentVersion, isImmOrOwnedObj } from './utils';
12-
import { GasData, normalizeSuiAddress, normalizeSuiObjectId, SuiObjectRef } from './mystenlab/types';
12+
import { normalizeSuiAddress, normalizeSuiObjectId, SuiObjectRef } from './mystenlab/types';
1313
import { SIGNATURE_SCHEME_BYTES } from './constants';
1414
import { Buffer } from 'buffer';
1515
import { fromB64, toB64 } from '@mysten/bcs';
@@ -23,7 +23,9 @@ import { hashTypedData } from './mystenlab/cryptography/hash';
2323
export abstract class Transaction<T> extends BaseTransaction {
2424
protected _suiTransaction: SuiTransaction<T>;
2525
protected _signature: Signature;
26+
protected _feePayerSignature: Signature;
2627
private _serializedSig: Uint8Array;
28+
private _serializedFeePayerSig: Uint8Array;
2729

2830
protected constructor(_coinConfig: Readonly<CoinConfig>) {
2931
super(_coinConfig);
@@ -48,17 +50,31 @@ export abstract class Transaction<T> extends BaseTransaction {
4850
addSignature(publicKey: BasePublicKey, signature: Buffer): void {
4951
this._signatures.push(signature.toString('hex'));
5052
this._signature = { publicKey, signature };
53+
this.setSerializedSig(publicKey, signature);
5154
this.serialize();
5255
}
5356

57+
addFeePayerSignature(publicKey: BasePublicKey, signature: Buffer): void {
58+
this._feePayerSignature = { publicKey, signature };
59+
this.setSerializedFeePayerSig(publicKey, signature);
60+
}
61+
5462
get suiSignature(): Signature {
5563
return this._signature;
5664
}
5765

66+
get feePayerSignature(): Signature {
67+
return this._feePayerSignature;
68+
}
69+
5870
get serializedSig(): Uint8Array {
5971
return this._serializedSig;
6072
}
6173

74+
get serializedFeePayerSig(): Uint8Array {
75+
return this._serializedFeePayerSig;
76+
}
77+
6278
setSerializedSig(publicKey: BasePublicKey, signature: Buffer): void {
6379
const pubKey = Buffer.from(publicKey.pub, 'hex');
6480
const serialized_sig = new Uint8Array(1 + signature.length + pubKey.length);
@@ -68,6 +84,15 @@ export abstract class Transaction<T> extends BaseTransaction {
6884
this._serializedSig = serialized_sig;
6985
}
7086

87+
setSerializedFeePayerSig(publicKey: BasePublicKey, signature: Buffer): void {
88+
const pubKey = Buffer.from(publicKey.pub, 'hex');
89+
const serialized_sig = new Uint8Array(1 + signature.length + pubKey.length);
90+
serialized_sig.set(SIGNATURE_SCHEME_BYTES);
91+
serialized_sig.set(signature, 1);
92+
serialized_sig.set(pubKey, 1 + signature.length);
93+
this._serializedFeePayerSig = serialized_sig;
94+
}
95+
7196
/** @inheritdoc */
7297
canSign(key: BaseKey): boolean {
7398
return true;
@@ -78,7 +103,6 @@ export abstract class Transaction<T> extends BaseTransaction {
78103
*
79104
* @param {KeyPair} signer key
80105
*/
81-
82106
sign(signer: KeyPair): void {
83107
if (!this._suiTransaction) {
84108
throw new InvalidTransactionError('empty transaction to sign');
@@ -87,10 +111,29 @@ export abstract class Transaction<T> extends BaseTransaction {
87111
const intentMessage = this.signablePayload;
88112
const signature = signer.signMessageinUint8Array(intentMessage);
89113

90-
this.setSerializedSig({ pub: signer.getKeys().pub }, Buffer.from(signature));
91114
this.addSignature({ pub: signer.getKeys().pub }, Buffer.from(signature));
92115
}
93116

117+
/**
118+
* Sign this transaction as a fee payer
119+
*
120+
* @param {KeyPair} signer key
121+
*/
122+
signFeePayer(signer: KeyPair): void {
123+
if (!this._suiTransaction) {
124+
throw new InvalidTransactionError('empty transaction to sign');
125+
}
126+
127+
if (!this._suiTransaction.gasData.sponsor) {
128+
throw new InvalidTransactionError('transaction does not have a fee payer');
129+
}
130+
131+
const intentMessage = this.signablePayload;
132+
const signature = signer.signMessageinUint8Array(intentMessage);
133+
134+
this.addFeePayerSignature({ pub: signer.getKeys().pub }, Buffer.from(signature));
135+
}
136+
94137
/** @inheritdoc */
95138
toBroadcastFormat(): string {
96139
if (!this._suiTransaction) {
@@ -178,6 +221,9 @@ export abstract class Transaction<T> extends BaseTransaction {
178221
owner: normalizeSuiAddress(transactionBlock.gasConfig.owner!),
179222
price: Number(transactionBlock.gasConfig.price as string),
180223
budget: Number(transactionBlock.gasConfig.budget as string),
224+
...(transactionBlock.gasConfig.sponsor && {
225+
sponsor: normalizeSuiAddress(transactionBlock.gasConfig.sponsor),
226+
}),
181227
},
182228
};
183229
}
@@ -213,12 +259,20 @@ export abstract class Transaction<T> extends BaseTransaction {
213259
}
214260

215261
static getProperGasData(k: any): GasData {
216-
return {
217-
payment: [this.normalizeSuiObjectRef(k.gasData.payment)],
262+
const gasData: GasData = {
263+
payment: Array.isArray(k.gasData.payment)
264+
? k.gasData.payment.map((p: any) => this.normalizeSuiObjectRef(p))
265+
: [this.normalizeSuiObjectRef(k.gasData.payment)],
218266
owner: utils.normalizeHexId(k.gasData.owner),
219267
price: Number(k.gasData.price),
220268
budget: Number(k.gasData.budget),
221269
};
270+
271+
if (k.gasData.sponsor) {
272+
gasData.sponsor = utils.normalizeHexId(k.gasData.sponsor);
273+
}
274+
275+
return gasData;
222276
}
223277

224278
private static normalizeCoins(coins: any[]): SuiObjectRef[] {
@@ -267,4 +321,15 @@ export abstract class Transaction<T> extends BaseTransaction {
267321

268322
return inputGasPaymentObjects;
269323
}
324+
325+
hasFeePayerSig(): boolean {
326+
return !!this._feePayerSignature;
327+
}
328+
329+
getFeePayerPubKey(): string | undefined {
330+
if (!this._feePayerSignature || !this._feePayerSignature.publicKey) {
331+
return undefined;
332+
}
333+
return this._feePayerSignature.publicKey.pub;
334+
}
270335
}

modules/sdk-coin-sui/src/lib/transactionBuilder.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,11 @@ export abstract class TransactionBuilder<T = SuiProgrammableTransaction> extends
5252
protected signImplementation(key: BaseKey): Transaction<T> {
5353
const signer = new KeyPair({ prv: key.key });
5454
this._signer = signer;
55-
this.transaction.sign(signer);
55+
const signable = this.transaction.signablePayload;
56+
const signature = signer.signMessageinUint8Array(signable);
57+
const signatureBuffer = Buffer.from(signature);
58+
this.transaction.addSignature({ pub: signer.getKeys().pub }, signatureBuffer);
59+
this.transaction.setSerializedSig({ pub: signer.getKeys().pub }, signatureBuffer);
5660
return this.transaction;
5761
}
5862

modules/sdk-coin-sui/src/lib/utils.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,16 @@ export class Utils implements BaseUtils {
549549
return result.digest;
550550
}
551551

552+
async executeTransactionBlockWithMultipleSigners(
553+
url: string,
554+
serializedTx: string,
555+
senderSignature: string,
556+
feePayerSignature?: string
557+
): Promise<string> {
558+
const signatures = feePayerSignature ? [senderSignature, feePayerSignature] : [senderSignature];
559+
return this.executeTransactionBlock(url, serializedTx, signatures);
560+
}
561+
552562
validateNonNegativeNumber(defaultVal: number, errorMsg: string, inputVal?: number): number {
553563
if (inputVal === undefined) {
554564
return defaultVal;

modules/sdk-coin-sui/src/sui.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ export interface SuiParsedTransaction extends ParsedTransaction {
8181

8282
export type SuiTransactionExplanation = TransactionExplanation;
8383

84+
export interface SuiMPCTx extends MPCTx {
85+
feePayerSignature?: string;
86+
}
87+
8488
export class Sui extends BaseCoin {
8589
protected readonly _staticsCoin: Readonly<StaticsBaseCoin>;
8690
protected constructor(bitgo: BitGoBase, staticsCoin?: Readonly<StaticsBaseCoin>) {
@@ -647,9 +651,18 @@ export class Sui extends BaseCoin {
647651
const url = this.getPublicNodeUrl();
648652
let digest = '';
649653
if (!!transactions) {
650-
for (const txn of transactions) {
654+
for (const txn of transactions as SuiMPCTx[]) {
651655
try {
652-
digest = await utils.executeTransactionBlock(url, txn.serializedTx, [txn.signature!]);
656+
if (txn.feePayerSignature) {
657+
digest = await utils.executeTransactionBlockWithMultipleSigners(
658+
url,
659+
txn.serializedTx,
660+
txn.signature!,
661+
txn.feePayerSignature
662+
);
663+
} else {
664+
digest = await utils.executeTransactionBlock(url, txn.serializedTx, [txn.signature!]);
665+
}
653666
} catch (e) {
654667
throw new Error(`Failed to broadcast transaction, error: ${e.message}`);
655668
}

0 commit comments

Comments
 (0)