Skip to content

Commit 194d215

Browse files
authored
Merge pull request #6743 from BitGo/win-6483-unsigned-sweep-xtz
feat: add unsigned sweep for wrw xtz
2 parents ff67004 + 20a4953 commit 194d215

File tree

3 files changed

+371
-16
lines changed

3 files changed

+371
-16
lines changed

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

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -330,10 +330,10 @@ export class Xtz extends BaseCoin {
330330
* @param params
331331
*/
332332
async recover(params: RecoverOptions): Promise<unknown> {
333+
const isUnsignedSweep = params.isUnsignedSweep;
333334
this.validateRecoveryParams(params);
334335

335336
// Clean up whitespace from entered values
336-
337337
const backupKey = params.backupKey.replace(/\s/g, '');
338338

339339
const userAddressDetails = await this.getAddressDetails(params.walletContractAddress, params.apiKey);
@@ -344,26 +344,32 @@ export class Xtz extends BaseCoin {
344344

345345
// Decrypt backup private key and get address
346346
let backupPrv;
347+
let keyPair;
348+
let backupSigningKey;
347349

348-
try {
349-
backupPrv = this.bitgo.decrypt({
350-
input: backupKey,
351-
password: params.walletPassphrase,
352-
});
353-
} catch (e) {
354-
throw new Error(`Error decrypting backup keychain: ${e.message}`);
355-
}
356-
const keyPair = new KeyPair({ prv: backupPrv });
357-
const backupSigningKey = keyPair.getKeys().prv;
358-
if (!backupSigningKey) {
359-
throw new Error('no private key');
350+
if (isUnsignedSweep) {
351+
keyPair = new KeyPair({ pub: backupKey });
352+
} else {
353+
try {
354+
backupPrv = this.bitgo.decrypt({
355+
input: backupKey,
356+
password: params.walletPassphrase,
357+
});
358+
} catch (e) {
359+
throw new Error(`Error decrypting backup keychain: ${e.message}`);
360+
}
361+
keyPair = new KeyPair({ prv: backupPrv });
362+
backupSigningKey = keyPair.getKeys().prv;
363+
if (!backupSigningKey) {
364+
throw new Error('no private key');
365+
}
360366
}
361-
const backupKeyAddress = keyPair.getAddress();
362367

368+
const backupKeyAddress = keyPair.getAddress();
363369
const backupAddressDetails = await this.getAddressDetails(backupKeyAddress, params.apiKey || '');
364370

365371
if (!backupAddressDetails.counter || !backupAddressDetails.balance) {
366-
throw new Error(`Missing required detail(s): counter, balance`);
372+
throw new Error(`Missing required detail(s) for ${backupKeyAddress}: counter, balance`);
367373
}
368374
const backupKeyNonce = new BigNumber(backupAddressDetails.counter + 1, 10);
369375

@@ -445,6 +451,21 @@ export class Xtz extends BaseCoin {
445451
.gasLimit(gasLimit.toString())
446452
.dataToSign(packedDataToSign);
447453

454+
if (isUnsignedSweep) {
455+
const tx = await txBuilder.build();
456+
const txInfo = tx.toJson();
457+
458+
return {
459+
txHex: tx.toBroadcastFormat(),
460+
txInfo,
461+
source: params.walletContractAddress,
462+
dataToSign: packedDataToSign,
463+
feeInfo,
464+
sourceCounter: backupKeyNonce.toString(),
465+
transferCounters: [backupKeyNonce.toString()],
466+
};
467+
}
468+
448469
txBuilder.sign({ key: backupSigningKey });
449470
const signedTx = await txBuilder.build();
450471

modules/sdk-coin-xtz/test/fixtures.ts

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,3 +394,291 @@ export const packDataToSign = {
394394
'0507070a0000001601bda70b50cf607aee95c10322a8bff9fc4df50f85000707009b9ad933050502000000350320053d036d0743035d0a0000001501c0d2caabc69b734fc54453ba5fab901b4fbec41b031e0743036a009482fd11034f034d031b',
395395
gas: 'unaccounted',
396396
};
397+
398+
export const paramsDetailsForUnsignedSweepRecovery = {
399+
userKey:
400+
'xpub661MyMwAqRbcGoAY7mg48rRvzTYyhNVjgLxHRxiTkga4TaGoaFtCwoDYWm7UMExhvxCBZ5Wf5o25n52iXVvwuUH53NYnDE1j3KJjhn7EdxA',
401+
backupKey:
402+
'xpub661MyMwAqRbcFjZuqhcenh2wHQgweVaBWvGTzwRXhkozW5XoVdyLkzbeenv8RHeA5YD6xu8DgHLQE4Tatsn126SSNnbP3GHKuZy3j6YiZvp',
403+
walletContractAddress: 'KT1Vwe7wFy6JmspMv4UmFFJU3JLtbfghBTBM',
404+
recoveryDestination: 'tz2RtnvELVAW5dUTtsBNG6cbSA2eQFiYt4Rp',
405+
feeAddress: 'tz2Qz5WCPw6VcBUjV9fRf15YgZxYcD6NsHeC',
406+
};
407+
408+
export const unsignedSweepContractDetails = {
409+
id: 714869,
410+
type: 'contract',
411+
address: 'KT1Vwe7wFy6JmspMv4UmFFJU3JLtbfghBTBM',
412+
kind: 'smart_contract',
413+
balance: 1000000,
414+
creator: {
415+
address: 'tz2JAsQV4rzh7z5FQrnL2ZFYaCwnD2tqWG74',
416+
},
417+
delegate: {
418+
alias: 'A Kishino',
419+
address: 'tz1aWXP237BLwNHJcCD4b3DutCevhqq2T1Z9',
420+
active: true,
421+
},
422+
delegationLevel: 14060554,
423+
delegationTime: '2025-07-31T05:13:16Z',
424+
numContracts: 0,
425+
tokensCount: 0,
426+
activeTokensCount: 0,
427+
tokenBalancesCount: 0,
428+
tokenTransfersCount: 0,
429+
ticketsCount: 0,
430+
activeTicketsCount: 0,
431+
ticketBalancesCount: 0,
432+
ticketTransfersCount: 0,
433+
numDelegations: 0,
434+
numOriginations: 1,
435+
numTransactions: 1,
436+
numReveals: 0,
437+
numMigrations: 0,
438+
transferTicketCount: 0,
439+
increasePaidStorageCount: 0,
440+
eventsCount: 0,
441+
firstActivity: 14060554,
442+
firstActivityTime: '2025-07-31T05:13:16Z',
443+
lastActivity: 14464067,
444+
lastActivityTime: '2025-08-19T09:04:23Z',
445+
typeHash: -486012176,
446+
codeHash: -655621303,
447+
};
448+
449+
export const unsignedSweepContractDetails2 = {
450+
id: 714869,
451+
type: 'contract',
452+
address: 'KT1Vwe7wFy6JmspMv4UmFFJU3JLtbfghBTBM',
453+
kind: 'smart_contract',
454+
balance: 1000000,
455+
creator: {
456+
address: 'tz2JAsQV4rzh7z5FQrnL2ZFYaCwnD2tqWG74',
457+
},
458+
delegate: {
459+
alias: 'A Kishino',
460+
address: 'tz1aWXP237BLwNHJcCD4b3DutCevhqq2T1Z9',
461+
active: true,
462+
},
463+
delegationLevel: 14060554,
464+
delegationTime: '2025-07-31T05:13:16Z',
465+
numContracts: 0,
466+
tokensCount: 0,
467+
activeTokensCount: 0,
468+
tokenBalancesCount: 0,
469+
tokenTransfersCount: 0,
470+
ticketsCount: 0,
471+
activeTicketsCount: 0,
472+
ticketBalancesCount: 0,
473+
ticketTransfersCount: 0,
474+
numDelegations: 0,
475+
numOriginations: 1,
476+
numTransactions: 1,
477+
numReveals: 0,
478+
numMigrations: 0,
479+
transferTicketCount: 0,
480+
increasePaidStorageCount: 0,
481+
eventsCount: 0,
482+
firstActivity: 14060554,
483+
firstActivityTime: '2025-07-31T05:13:16Z',
484+
lastActivity: 14464067,
485+
lastActivityTime: '2025-08-19T09:04:23Z',
486+
typeHash: -486012176,
487+
codeHash: -655621303,
488+
};
489+
490+
export const unsignedSweepFeeAddressDetails = {
491+
id: 715922,
492+
type: 'user',
493+
address: 'tz2Qz5WCPw6VcBUjV9fRf15YgZxYcD6NsHeC',
494+
revealed: false,
495+
balance: 20000000,
496+
rollupBonds: 0,
497+
smartRollupBonds: 0,
498+
stakedBalance: 0,
499+
unstakedBalance: 0,
500+
counter: 54996302,
501+
numContracts: 0,
502+
rollupsCount: 0,
503+
smartRollupsCount: 0,
504+
activeTokensCount: 0,
505+
tokenBalancesCount: 0,
506+
tokenTransfersCount: 0,
507+
activeTicketsCount: 0,
508+
ticketBalancesCount: 0,
509+
ticketTransfersCount: 0,
510+
numActivations: 0,
511+
numDelegations: 0,
512+
numOriginations: 0,
513+
numTransactions: 1,
514+
numReveals: 0,
515+
numRegisterConstants: 0,
516+
numSetDepositsLimits: 0,
517+
numMigrations: 0,
518+
txRollupOriginationCount: 0,
519+
txRollupSubmitBatchCount: 0,
520+
txRollupCommitCount: 0,
521+
txRollupReturnBondCount: 0,
522+
txRollupFinalizeCommitmentCount: 0,
523+
txRollupRemoveCommitmentCount: 0,
524+
txRollupRejectionCount: 0,
525+
txRollupDispatchTicketsCount: 0,
526+
transferTicketCount: 0,
527+
increasePaidStorageCount: 0,
528+
drainDelegateCount: 0,
529+
smartRollupAddMessagesCount: 0,
530+
smartRollupCementCount: 0,
531+
smartRollupExecuteCount: 0,
532+
smartRollupOriginateCount: 0,
533+
smartRollupPublishCount: 0,
534+
smartRollupRecoverBondCount: 0,
535+
smartRollupRefuteCount: 0,
536+
refutationGamesCount: 0,
537+
activeRefutationGamesCount: 0,
538+
stakingOpsCount: 0,
539+
stakingUpdatesCount: 0,
540+
setDelegateParametersOpsCount: 0,
541+
dalPublishCommitmentOpsCount: 0,
542+
firstActivity: 14321474,
543+
firstActivityTime: '2025-08-12T11:12:05Z',
544+
lastActivity: 14321474,
545+
lastActivityTime: '2025-08-12T11:12:05Z',
546+
lostBalance: 0,
547+
};
548+
549+
export const unsignedSweepDataToPack = {
550+
data: {
551+
prim: 'Pair',
552+
args: [
553+
{
554+
string: 'KT1Vwe7wFy6JmspMv4UmFFJU3JLtbfghBTBM',
555+
},
556+
{
557+
prim: 'Pair',
558+
args: [
559+
{
560+
int: '54996303',
561+
},
562+
{
563+
prim: 'Left',
564+
args: [
565+
[
566+
{
567+
prim: 'DROP',
568+
},
569+
{
570+
prim: 'NIL',
571+
args: [
572+
{
573+
prim: 'operation',
574+
},
575+
],
576+
},
577+
{
578+
prim: 'PUSH',
579+
args: [
580+
{
581+
prim: 'key_hash',
582+
},
583+
{
584+
string: 'tz2RtnvELVAW5dUTtsBNG6cbSA2eQFiYt4Rp',
585+
},
586+
],
587+
},
588+
{
589+
prim: 'IMPLICIT_ACCOUNT',
590+
},
591+
{
592+
prim: 'PUSH',
593+
args: [
594+
{
595+
prim: 'mutez',
596+
},
597+
{
598+
int: '1000000',
599+
},
600+
],
601+
},
602+
{
603+
prim: 'UNIT',
604+
},
605+
{
606+
prim: 'TRANSFER_TOKENS',
607+
},
608+
{
609+
prim: 'CONS',
610+
},
611+
],
612+
],
613+
},
614+
],
615+
},
616+
],
617+
},
618+
type: {
619+
prim: 'pair',
620+
args: [
621+
{
622+
prim: 'address',
623+
},
624+
{
625+
prim: 'pair',
626+
args: [
627+
{
628+
prim: 'nat',
629+
annots: ['%counter'],
630+
},
631+
{
632+
prim: 'or',
633+
args: [
634+
{
635+
prim: 'lambda',
636+
args: [
637+
{
638+
prim: 'unit',
639+
},
640+
{
641+
prim: 'list',
642+
args: [
643+
{
644+
prim: 'operation',
645+
},
646+
],
647+
},
648+
],
649+
annots: ['%operation'],
650+
},
651+
{
652+
prim: 'pair',
653+
args: [
654+
{
655+
prim: 'nat',
656+
annots: ['%threshold'],
657+
},
658+
{
659+
prim: 'list',
660+
args: [
661+
{
662+
prim: 'key',
663+
},
664+
],
665+
annots: ['%keys'],
666+
},
667+
],
668+
annots: ['%change_keys'],
669+
},
670+
],
671+
annots: [':action'],
672+
},
673+
],
674+
annots: [':payload'],
675+
},
676+
],
677+
},
678+
};
679+
680+
export const unsignedSweepPackDataToSign = {
681+
packed:
682+
'0507070a0000001601ea4d295f3387e9efbf6d791475cb936839eb8d58000707008fb5b934050502000000340320053d036d0743035d0a0000001501c0d2caabc69b734fc54453ba5fab901b4fbec41b031e0743036a0080897a034f034d031b',
683+
gas: 'unaccounted',
684+
};

0 commit comments

Comments
 (0)