import { IntegerType } from '@stacks/common';
import { ClarityValue } from './clarity';
import {
FungibleComparator,
FungiblePostCondition,
NonFungibleComparator,
NonFungiblePostCondition,
PostCondition,
StxPostCondition,
} from './postcondition-types';
import { AddressString, AssetString, ContractIdString } from './types';
import { parseContractId, validateStacksAddress } from './utils';
import { deserializePostConditionWire } from './wire';
import { parsePostConditionAmount, wireToPostCondition } from './postcondition';
/// `Pc.` Post Condition Builder
//
// This is a behavioral helper interface for constructing post conditions.
//
// The general pattern is:
// PRINCIPAL -> [AMOUNT] -> CODE -> ASSET
//
/**
* ### `Pc.` Post Condition Builder
* @beta Interface may be subject to change in future releases.
* @param {AddressString | ContractIdString} principal The principal to check, which should/should-not be sending assets. A string in the format `
` or `.`.
* @returns A partial post condition builder, which can be chained into a final post condition.
* @example
* ```
* import { Pc } from '@stacks/transactions';
* Pc.principal('STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6').willSendEq(10000).ustx();
* Pc.principal('STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6.mycontract').willSendGte(2000).ft();
* ```
*/
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
export function principal(principal: AddressString | ContractIdString) {
const [address, name] = principal.split('.');
// todo: improve validity check (and add helper methods like `isValidContractId`, `isValidAdress`,
// token name, asset syntax, etc.) -- also deupe .split checks in codebase
if (!address || !validateStacksAddress(address) || (typeof name === 'string' && !name)) {
throw new Error(`Invalid contract id: ${principal}`);
}
return new PartialPcWithPrincipal(principal);
}
/**
* ### `Pc.` Post Condition Builder
* @beta Interface may be subject to change in future releases.
* @returns A partial post condition builder, which can be chained into a final post condition.
* @example
* ```
* import { Pc } from '@stacks/transactions';
* Pc.origin().willSendEq(10000).ustx();
* Pc.origin().willSendGte(2000).ft();
* ```
*/
export function origin() {
return new PartialPcWithPrincipal('origin');
}
/**
* Not meant to be used directly. Start from `Pc.principal(…)` instead.
*/
class PartialPcWithPrincipal {
constructor(private address: string) {}
// todo: split FT and STX into separate methods? e.g. `willSendSTXEq` and `willSendFtEq`
/**
* ### Fungible Token Post Condition
* A post-condition sending tokens `FungibleConditionCode.Equal` (equal to) the given amount of uSTX or fungible-tokens.
* Finalize with the chained `.ustx()` or `.ft(…)` method.
* @example
* ```
* import { Pc } from '@stacks/transactions';
* Pc.principal('STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6').willSendEq(100).stx();
* ```
*/
willSendEq(amount: IntegerType) {
return new PartialPcFtWithCode(this.address, amount, 'eq');
}
/**
* ### Fungible Token Post Condition
* A post-condition sending tokens `FungibleConditionCode.LessEqual` (less-than or equal to) the given amount of uSTX or fungible-tokens.
* Finalize with the chained `.ustx()` or `.ft(…)` method.
* @example
* ```
* import { Pc } from '@stacks/transactions';
* Pc.principal('STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6').willSendLte(100).stx();
* ```
*/
willSendLte(amount: IntegerType) {
return new PartialPcFtWithCode(this.address, amount, 'lte');
}
/**
* ### Fungible Token Post Condition
* A post-condition sending tokens `FungibleConditionCode.Less` (less-than) the given amount of uSTX or fungible-tokens.
* Finalize with the chained `.ustx()` or `.ft(…)` method.
* @example
* ```
* import { Pc } from '@stacks/transactions';
* Pc.principal('STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6').willSendLt(100).stx();
* ```
*/
willSendLt(amount: IntegerType) {
return new PartialPcFtWithCode(this.address, amount, 'lt');
}
/**
* ### Fungible Token Post Condition
* A post-condition sending tokens `FungibleConditionCode.GreaterEqual` (greater-than or equal to) the given amount of uSTX or fungible-tokens.
* Finalize with the chained `.ustx()` or `.ft(…)` method.
* @example
* ```
* import { Pc } from '@stacks/transactions';
* Pc.principal('STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6').willSendGte(100).stx();
* ```
*/
willSendGte(amount: IntegerType) {
return new PartialPcFtWithCode(this.address, amount, 'gte');
}
/**
* ### Fungible Token Post Condition
* A post-condition sending tokens `FungibleConditionCode.Greater` (greater-than) the given amount of uSTX or fungible-tokens.
* Finalize with the chained `.ustx()` or `.ft(…)` method.
* @example
* ```
* import { Pc } from '@stacks/transactions';
* Pc.principal('STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6').willSendGt(100).stx();
* ```
*/
willSendGt(amount: IntegerType) {
return new PartialPcFtWithCode(this.address, amount, 'gt');
}
/**
* ### NFT Post Condition
* A post-condition which `NonFungibleConditionCode.Sends` an NFT.
* Finalize with the chained `.nft(…)` method.
* @example
* ```
* import { Pc } from '@stacks/transactions';
* Pc.principal('STB4…K6.nft-contract').willSendAsset().nft('STB4…K6.super-nft::super', uintCV(1));
* ```
*/
willSendAsset() {
return new PartialPcNftWithCode(this.address, 'sent');
}
/**
* ### NFT Post Condition
* A post-condition which `NonFungibleConditionCode.DoesNotSend` an NFT.
* Finalize with the chained `.nft(…)` method.
* @example
* ```
* import { Pc } from '@stacks/transactions';
* Pc.principal('STB4…K6.nft-contract').willNotSendAsset().nft('STB4…K6.super-nft::super', uintCV(1));
* ```
*/
willNotSendAsset() {
return new PartialPcNftWithCode(this.address, 'not-sent');
}
/**
* ### NFT Post Condition
* A post-condition where the NFT `NonFungibleConditionCode.MaybeSent` (may or may not be sent).
* Finalize with the chained `.nft(…)` method.
*
* **⚠︎ Attention**: Enabled with [Epoch 3.4](https://forum.stacks.org/t/clarity-5-and-epoch-3-4/18659)
*
* @see [SIP-039](https://github.com/stacksgov/sips/pull/256/changes)
* @see [SIP-040](https://github.com/stacksgov/sips/pull/257/changes)
*/
willMaybeSendAsset() {
return new PartialPcNftWithCode(this.address, 'maybe-sent');
}
}
/**
* Not meant to be used directly. Start from `Pc.principal(…)` instead.
*/
class PartialPcFtWithCode {
constructor(
private address: string,
private amount: IntegerType,
private code: FungibleComparator
) {}
/**
* ### STX Post Condition
* ⚠ Amount of STX is denoted in uSTX (micro-STX)
*/
ustx(): StxPostCondition {
// todo: rename to `uSTX`?
return {
type: 'stx-postcondition',
address: this.address,
condition: this.code,
amount: parsePostConditionAmount(this.amount).toString(),
};
}
/**
* ### Fungible Token Post Condition
* ⚠ Amount of fungible tokens is denoted in the smallest unit of the token
*/
ft(contractId: ContractIdString, tokenName: string): FungiblePostCondition {
// todo: allow taking one arg (`Asset`) as well, overload
const [address, name] = contractId.split('.');
if (!address || !validateStacksAddress(address) || (typeof name === 'string' && !name)) {
throw new Error(`Invalid contract id: ${contractId}`);
}
return {
type: 'ft-postcondition',
address: this.address,
condition: this.code,
amount: parsePostConditionAmount(this.amount).toString(),
asset: `${contractId}::${tokenName}`,
};
}
}
/**
* Not meant to be used directly. Start from `Pc.principal(…)` instead.
*/
class PartialPcNftWithCode {
constructor(
private address: string,
private code: NonFungibleComparator
) {}
/**
* ### Non-Fungible Token Post Condition
* @param assetName - The name of the NFT asset. Formatted as `.::`.
* @param assetId - The asset identifier of the NFT. A Clarity value defining the single NFT instance.
*/
nft(assetName: AssetString, assetId: ClarityValue): NonFungiblePostCondition;
/**
* ### Non-Fungible Token Post Condition
* @param contractId - The contract identifier of the NFT. Formatted as `.`.
* @param tokenName - The name of the NFT asset.
* @param assetId - The asset identifier of the NFT. A Clarity value defining the single NFT instance.
*/
nft(
contractId: ContractIdString,
tokenName: string,
assetId: ClarityValue
): NonFungiblePostCondition;
nft(...args: [any, any] | [any, any, any]): NonFungiblePostCondition {
const { contractAddress, contractName, tokenName, assetId } = getNftArgs(
...(args as [any, any, any])
);
if (!validateStacksAddress(contractAddress)) {
throw new Error(`Invalid contract id: ${contractAddress}`);
}
return {
type: 'nft-postcondition',
address: this.address,
condition: this.code,
asset: `${contractAddress}.${contractName}::${tokenName}`,
assetId,
};
}
}
/** @internal */
function parseNft(nftAssetName: AssetString) {
const [principal, tokenName] = nftAssetName.split('::') as [ContractIdString, string];
if (!principal || !tokenName)
throw new Error(`Invalid fully-qualified nft asset name: ${nftAssetName}`);
const [address, name] = parseContractId(principal);
return { contractAddress: address, contractName: name, tokenName };
}
/**
* Deserializes a serialized post condition hex string into a post condition object
* @param hex - Post condition hex string
* @returns Deserialized post condition
* @example
* ```ts
* import { Pc } from '@stacks/transactions';
*
* const hex = '00021600000000000000000000000000000000000000000200000000000003e8'
* const postCondition = Pc.fromHex(hex);
* // {
* // type: 'stx-postcondition',
* // address: 'SP000000000000000000002Q6VF78',
* // condition: 'gt',
* // amount: '1000'
* // }
* ```
*/
export function fromHex(hex: string): PostCondition {
const wire = deserializePostConditionWire(hex);
return wireToPostCondition(wire);
}
/**
* Helper method for `PartialPcNftWithCode.nft` to parse the arguments.
* @internal
*/
function getNftArgs(
asset: AssetString,
assetId: ClarityValue
): { contractAddress: string; contractName: string; tokenName: string; assetId: ClarityValue };
function getNftArgs(
contractId: ContractIdString,
tokenName: string,
assetId: ClarityValue
): { contractAddress: string; contractName: string; tokenName: string; assetId: ClarityValue };
function getNftArgs(...args: [any, any] | [any, any, any]): {
contractAddress: string;
contractName: string;
tokenName: string;
assetId: ClarityValue;
} {
if (args.length === 2) {
const [assetName, assetId] = args;
return { ...parseNft(assetName), assetId };
}
// args.length === 3
const [contractId, tokenName, assetId] = args;
const [address, name] = parseContractId(contractId);
return { contractAddress: address, contractName: name, tokenName, assetId };
}