From 635e34ba0acd641f6b65f48f0f723c77ebee3302 Mon Sep 17 00:00:00 2001 From: Lazy Nina Date: Mon, 29 Jan 2024 18:34:01 -0500 Subject: [PATCH] Add support for lockup txn construction in deso-js --- src/identity/crypto-utils.ts | 17 ++ src/identity/transaction-transcoders.ts | 60 +++++ src/identity/transcoders.ts | 7 + src/transactions/lockup.ts | 335 ++++++++++++++++++++++++ 4 files changed, 419 insertions(+) create mode 100644 src/transactions/lockup.ts diff --git a/src/identity/crypto-utils.ts b/src/identity/crypto-utils.ts index 329c33a..c58961b 100644 --- a/src/identity/crypto-utils.ts +++ b/src/identity/crypto-utils.ts @@ -78,6 +78,23 @@ export const uint64ToBufBigEndian = (uint: number) => { return new Uint8Array(result.reverse()); }; +export const varint64ToBuf = (int: number) => { + let ux = BigInt(int) << BigInt(1); + if (int < 0) { + ux = ~ux; + } + return uvarint64ToBuf(Number(ux)); +}; + +export const bufToVarint64 = (buffer: Uint8Array): [number, Uint8Array] => { + const [ux, n] = bufToUvarint64(buffer); + let x = BigInt(ux) >> BigInt(1); + if (ux & 1) { + x = ~x; + } + return [Number(x), n]; +}; + interface Base58CheckOptions { network: Network; } diff --git a/src/identity/transaction-transcoders.ts b/src/identity/transaction-transcoders.ts index 6eea3cd..df41635 100644 --- a/src/identity/transaction-transcoders.ts +++ b/src/identity/transaction-transcoders.ts @@ -23,6 +23,7 @@ import { instanceToType, VarBufferArray, BoolOptional, + Varint64, } from './transcoders.js'; export class TransactionInput extends BinaryRecord { @Transcode(FixedBuffer(32)) @@ -662,6 +663,61 @@ export class TransactionMetadataUnlockStake extends BinaryRecord { export class TransactionMetadataUnjailValidator extends BinaryRecord {} +export class TransactionMetadataCoinLockup extends BinaryRecord { + @Transcode(VarBuffer) + profilePublicKey: Uint8Array = new Uint8Array(0); + + @Transcode(VarBuffer) + recipientPublicKey: Uint8Array = new Uint8Array(0); + + @Transcode(Varint64) + unlockTimestampNanoSecs = 0; + + @Transcode(Varint64) + vestingEndTimestampNanoSecs = 0; + + // TODO: We may want a better way to handle uint256s. + @Transcode(BoolOptional(VarBuffer)) + lockupAmountBaseUnits: Uint8Array = new Uint8Array(0); +} + +export class TransactionMetadataUpdateCoinLockupParams extends BinaryRecord { + @Transcode(Varint64) + lockupYieldDurationNanoSecs = 0; + + @Transcode(Uvarint64) + lockupYieldAPYBasisPoints = 0; + + @Transcode(Boolean) + removeYieldCurvePoint = false; + + @Transcode(Boolean) + newLockupTransferRestrictions = false; + + @Transcode(Uint8) + lockupTransferRestrictionStatus = 0; +} + +export class TransactionMetadataCoinLockupTransfer extends BinaryRecord { + @Transcode(VarBuffer) + recipientPublicKey: Uint8Array = new Uint8Array(0); + + @Transcode(VarBuffer) + profilePublicKey: Uint8Array = new Uint8Array(0); + + @Transcode(Varint64) + unlockTimestampNanoSecs = 0; + + // TODO: We may want a better way to handle uint256s. + @Transcode(BoolOptional(VarBuffer)) + lockedCoinsToTransferBaseUnits: Uint8Array = new Uint8Array(0); +} + +export class TransactionMetadataCoinUnlock extends BinaryRecord { + @Transcode(VarBuffer) + profilePublicKey: Uint8Array = new Uint8Array(0); +} + export const TransactionTypeMetadataMap = { 1: TransactionMetadataBlockReward, 2: TransactionMetadataBasicTransfer, @@ -743,6 +799,10 @@ export const TransactionTypeToStringMap: { [k: number]: string } = { 37: TransactionType.Unstake, 38: TransactionType.UnlockStake, 39: TransactionType.UnjailValidator, + 40: TransactionType.CoinLockup, + 41: TransactionType.UpdateCoinLockupParams, + 42: TransactionType.CoinLockupTransfer, + 43: TransactionType.CoinUnlock, }; export class Transaction extends BinaryRecord { diff --git a/src/identity/transcoders.ts b/src/identity/transcoders.ts index 54b063e..091e038 100644 --- a/src/identity/transcoders.ts +++ b/src/identity/transcoders.ts @@ -1,8 +1,10 @@ import 'reflect-metadata'; import { bufToUvarint64, + bufToVarint64, concatUint8Arrays, uvarint64ToBuf, + varint64ToBuf, } from './crypto-utils.js'; import { TransactionNonce } from './transaction-transcoders.js'; export class BinaryRecord { @@ -69,6 +71,11 @@ export const Uvarint64: Transcoder = { write: (uint) => uvarint64ToBuf(uint), }; +export const Varint64: Transcoder = { + read: (bytes) => bufToVarint64(bytes), + write: (int) => varint64ToBuf(int), +}; + export const Boolean: Transcoder = { read: (bytes) => [bytes.at(0) != 0, bytes.slice(1)], write: (bool) => { diff --git a/src/transactions/lockup.ts b/src/transactions/lockup.ts new file mode 100644 index 0000000..36efdf4 --- /dev/null +++ b/src/transactions/lockup.ts @@ -0,0 +1,335 @@ +import { + ConstructedAndSubmittedTx, + TxRequestOptions, + TypeWithOptionalFeesAndExtraData, +} from '../types.js'; +import { + CoinLockResponse, + CoinLockupRequest, + CoinLockupTransferRequest, + CoinUnlockRequest, + ConstructedTransactionResponse, + LockupLimitMapItem, + LockupLimitOperationString, + LockupLimitScopeType, + UpdateCoinLockupParamsRequest, +} from '../backend-types/index.js'; +import { + bs58PublicKeyToCompressedBytes, + TransactionMetadataCoinLockup, + TransactionMetadataCoinLockupTransfer, + TransactionMetadataCoinUnlock, + TransactionMetadataUpdateCoinLockupParams, +} from '../identity/index.js'; +import { hexToBytes } from '@noble/hashes/utils'; +import { + constructBalanceModelTx, + getTxWithFeeNanos, + handleSignAndSubmit, + sumTransactionFees, +} from '../internal.js'; +import { guardTxPermission } from './utils.js'; + +type CoinLockupRequestParams = + TypeWithOptionalFeesAndExtraData; + +const buildCoinLockupMetadata = (params: CoinLockupRequestParams) => { + const metadata = new TransactionMetadataCoinLockup(); + metadata.profilePublicKey = bs58PublicKeyToCompressedBytes( + params.ProfilePublicKeyBase58Check + ); + metadata.recipientPublicKey = bs58PublicKeyToCompressedBytes( + params.RecipientPublicKeyBase58Check + ); + // TODO: make sure this replace is correct. + metadata.lockupAmountBaseUnits = hexToBytes( + params.LockupAmountBaseUnits.replace('0x', 'x') + ); + metadata.unlockTimestampNanoSecs = params.UnlockTimestampNanoSecs; + metadata.vestingEndTimestampNanoSecs = params.VestingEndTimestampNanoSecs; + return metadata; +}; + +export const constructCoinLockupTransaction = ( + params: CoinLockupRequestParams +): Promise => { + return constructBalanceModelTx( + params.TransactorPublicKeyBase58Check, + buildCoinLockupMetadata(params), + { + ExtraData: params.ExtraData, + MinFeeRateNanosPerKB: params.MinFeeRateNanosPerKB, + TransactionFees: params.TransactionFees, + } + ); +}; + +export const coinLockup = async ( + params: CoinLockupRequestParams, + options?: TxRequestOptions +): Promise< + ConstructedAndSubmittedTx +> => { + const txWithFee = getTxWithFeeNanos( + params.TransactorPublicKeyBase58Check, + buildCoinLockupMetadata(params), + { + ExtraData: params.ExtraData, + MinFeeRateNanosPerKB: params.MinFeeRateNanosPerKB, + TransactionFees: params.TransactionFees, + } + ); + + if (options?.checkPermissions !== false) { + await guardTxPermission({ + GlobalDESOLimit: + txWithFee.feeNanos + sumTransactionFees(params.TransactionFees), + LockupLimitMap: [ + { + ProfilePublicKeyBase58Check: params.ProfilePublicKeyBase58Check, + Operation: LockupLimitOperationString.COIN_LOCKUP, + ScopeType: LockupLimitScopeType.SCOPED, + OpCount: options?.txLimitCount ?? 1, + }, + ], + }); + } + + return handleSignAndSubmit('api/v0/coin-lockup', params, { + ...options, + constructionFunction: constructCoinLockupTransaction, + }); +}; + +type CoinUnlockRequestParams = + TypeWithOptionalFeesAndExtraData; + +const buildCoinUnlockMetadata = (params: CoinUnlockRequestParams) => { + const metadata = new TransactionMetadataCoinUnlock(); + metadata.profilePublicKey = bs58PublicKeyToCompressedBytes( + params.ProfilePublicKeyBase58Check + ); + return metadata; +}; + +export const constructCoinUnlockTransaction = ( + params: CoinUnlockRequestParams +): Promise => { + return constructBalanceModelTx( + params.TransactorPublicKeyBase58Check, + buildCoinUnlockMetadata(params), + { + ExtraData: params.ExtraData, + MinFeeRateNanosPerKB: params.MinFeeRateNanosPerKB, + TransactionFees: params.TransactionFees, + } + ); +}; + +export const coinUnlock = async ( + params: CoinUnlockRequestParams, + options?: TxRequestOptions +): Promise< + ConstructedAndSubmittedTx +> => { + const txWithFee = getTxWithFeeNanos( + params.TransactorPublicKeyBase58Check, + buildCoinUnlockMetadata(params), + { + ExtraData: params.ExtraData, + MinFeeRateNanosPerKB: params.MinFeeRateNanosPerKB, + TransactionFees: params.TransactionFees, + } + ); + + if (options?.checkPermissions !== false) { + await guardTxPermission({ + GlobalDESOLimit: + txWithFee.feeNanos + sumTransactionFees(params.TransactionFees), + LockupLimitMap: [ + { + ProfilePublicKeyBase58Check: params.ProfilePublicKeyBase58Check, + Operation: LockupLimitOperationString.COIN_UNLOCK, + ScopeType: LockupLimitScopeType.SCOPED, + OpCount: options?.txLimitCount ?? 1, + }, + ], + }); + } + + return handleSignAndSubmit('api/v0/coin-unlock', params, { + ...options, + constructionFunction: constructCoinUnlockTransaction, + }); +}; + +type CoinLockupTransferRequestParams = + TypeWithOptionalFeesAndExtraData; + +const buildCoinLockupTransferMetadata = ( + params: CoinLockupTransferRequestParams +) => { + const metadata = new TransactionMetadataCoinLockupTransfer(); + metadata.profilePublicKey = bs58PublicKeyToCompressedBytes( + params.ProfilePublicKeyBase58Check + ); + metadata.recipientPublicKey = bs58PublicKeyToCompressedBytes( + params.RecipientPublicKeyBase58Check + ); + metadata.unlockTimestampNanoSecs = params.UnlockTimestampNanoSecs; + // TODO: make sure this replace is correct. + metadata.lockedCoinsToTransferBaseUnits = hexToBytes( + params.LockedCoinsToTransferBaseUnits.replace('0x', 'x') + ); + return metadata; +}; + +export const constructCoinLockupTransferTransaction = ( + params: CoinLockupTransferRequestParams +): Promise => { + return constructBalanceModelTx( + params.TransactorPublicKeyBase58Check, + buildCoinLockupTransferMetadata(params), + { + ExtraData: params.ExtraData, + MinFeeRateNanosPerKB: params.MinFeeRateNanosPerKB, + TransactionFees: params.TransactionFees, + } + ); +}; + +export const coinLockupTransfer = async ( + params: CoinLockupTransferRequestParams, + options?: TxRequestOptions +): Promise< + ConstructedAndSubmittedTx +> => { + const txWithFee = getTxWithFeeNanos( + params.TransactorPublicKeyBase58Check, + buildCoinLockupTransferMetadata(params), + { + ExtraData: params.ExtraData, + MinFeeRateNanosPerKB: params.MinFeeRateNanosPerKB, + TransactionFees: params.TransactionFees, + } + ); + + if (options?.checkPermissions !== false) { + await guardTxPermission({ + GlobalDESOLimit: + txWithFee.feeNanos + sumTransactionFees(params.TransactionFees), + LockupLimitMap: [ + { + ProfilePublicKeyBase58Check: params.ProfilePublicKeyBase58Check, + Operation: LockupLimitOperationString.COIN_LOCKUP_TRANSFER, + ScopeType: LockupLimitScopeType.SCOPED, + OpCount: options?.txLimitCount ?? 1, + }, + ], + }); + } + + return handleSignAndSubmit('api/v0/coin-lockup-transfer', params, { + ...options, + constructionFunction: constructCoinLockupTransferTransaction, + }); +}; + +type UpdateCoinLockupParamsRequestParams = + TypeWithOptionalFeesAndExtraData; + +const buildUpdateCoinLockupParamsMetadata = ( + params: UpdateCoinLockupParamsRequestParams +) => { + const metadata = new TransactionMetadataUpdateCoinLockupParams(); + metadata.lockupYieldDurationNanoSecs = params.LockupYieldDurationNanoSecs; + metadata.lockupYieldAPYBasisPoints = params.LockupYieldAPYBasisPoints; + metadata.removeYieldCurvePoint = params.RemoveYieldCurvePoint; + metadata.newLockupTransferRestrictions = params.NewLockupTransferRestrictions; + let transferRestrictionStatus: number; + switch (params.LockupTransferRestrictionStatus) { + case 'dao_members_only': + transferRestrictionStatus = 2; + break; + case 'permanently_unrestricted': + transferRestrictionStatus = 3; + break; + case 'profile_owner_only': + transferRestrictionStatus = 1; + break; + case 'unrestricted': + transferRestrictionStatus = 0; + break; + default: + throw new Error('Invalid LockupTransferRestrictionStatus'); + } + metadata.lockupTransferRestrictionStatus = transferRestrictionStatus; + return metadata; +}; + +export const constructUpdateCoinLockupParamsTransaction = ( + params: UpdateCoinLockupParamsRequestParams +): Promise => { + return constructBalanceModelTx( + params.TransactorPublicKeyBase58Check, + buildUpdateCoinLockupParamsMetadata(params), + { + ExtraData: params.ExtraData, + MinFeeRateNanosPerKB: params.MinFeeRateNanosPerKB, + TransactionFees: params.TransactionFees, + } + ); +}; + +export const updateCoinLockupParams = async ( + params: UpdateCoinLockupParamsRequestParams, + options?: TxRequestOptions +): Promise< + ConstructedAndSubmittedTx +> => { + const txWithFee = getTxWithFeeNanos( + params.TransactorPublicKeyBase58Check, + buildUpdateCoinLockupParamsMetadata(params), + { + ExtraData: params.ExtraData, + MinFeeRateNanosPerKB: params.MinFeeRateNanosPerKB, + TransactionFees: params.TransactionFees, + } + ); + + if (options?.checkPermissions !== false) { + // TODO: this one is tricky since a single transaction can reduce from two + // different limits. + // @jacksondean - help me plzzz. + const newLockupTransferRestrictionLimit = + params.NewLockupTransferRestrictions + ? { + ProfilePublicKeyBase58Check: params.TransactorPublicKeyBase58Check, + Operation: + LockupLimitOperationString.UPDATE_COIN_LOCKUP_TRANSFER_RESTRICTIONS, + ScopeType: LockupLimitScopeType.SCOPED, + OpCount: options?.txLimitCount ?? 1, + } + : null; + const addYieldCurvePointLimit = { + ProfilePublicKeyBase58Check: params.TransactorPublicKeyBase58Check, + Operation: LockupLimitOperationString.UPDATE_COIN_LOCKUP_YIELD_CURVE, + ScopeType: LockupLimitScopeType.SCOPED, + OpCount: options?.txLimitCount ?? 1, + }; + const limits = [addYieldCurvePointLimit]; + if (newLockupTransferRestrictionLimit) { + limits.push(newLockupTransferRestrictionLimit); + } + await guardTxPermission({ + GlobalDESOLimit: + txWithFee.feeNanos + sumTransactionFees(params.TransactionFees), + LockupLimitMap: limits, + }); + } + + return handleSignAndSubmit('api/v0/update-coin-lockup-params', params, { + ...options, + constructionFunction: constructUpdateCoinLockupParamsTransaction, + }); +};