diff --git a/sdk/example-app/src/extension-example.ts b/sdk/example-app/src/extension-example.ts index 78cf54c..d9de91b 100644 --- a/sdk/example-app/src/extension-example.ts +++ b/sdk/example-app/src/extension-example.ts @@ -37,7 +37,7 @@ async function main(): Promise { const tx = new Transaction(); - tx.add(client.pas.tx.sendBalance({ + tx.add(client.pas.call.sendBalance({ from: sender, // sender here. to: '0x2', // receiver here. amount: 1_000_000, // 1 demoUSD @@ -96,7 +96,7 @@ async function finalizeTestAssetSetup(client: PasClientType) { async function createAccountForAddress(client: PasClientType, address: string) { const tx = new Transaction(); - tx.add(client.pas.tx.accountForAddress(address)); + tx.add(client.pas.call.accountForAddress(address)); return signAndExecute(client, tx); } diff --git a/sdk/pas/src/client.ts b/sdk/pas/src/client.ts index 7f3a134..ac20e44 100644 --- a/sdk/pas/src/client.ts +++ b/sdk/pas/src/client.ts @@ -3,10 +3,7 @@ import type { ClientWithCoreApi } from '@mysten/sui/client'; -import { - MAINNET_PAS_PACKAGE_CONFIG, - TESTNET_PAS_PACKAGE_CONFIG, -} from './constants.js'; +import { MAINNET_PAS_PACKAGE_CONFIG, TESTNET_PAS_PACKAGE_CONFIG } from './constants.js'; import { deriveAccountAddress, derivePolicyAddress, @@ -134,7 +131,7 @@ export class PASClient { * that registers a `$Intent` placeholder in the transaction. The actual PTB commands * are resolved lazily at `tx.build()` time via the shared PAS resolver plugin. */ - get tx() { + get call() { return { /** * Creates a transfer funds intent. At build time, it auto-resolves the issuer's diff --git a/sdk/pas/src/contracts/pas/account.ts b/sdk/pas/src/contracts/pas/account.ts index edb152e..2e7cb12 100644 --- a/sdk/pas/src/contracts/pas/account.ts +++ b/sdk/pas/src/contracts/pas/account.ts @@ -67,8 +67,8 @@ export interface ShareOptions { arguments: ShareArguments | [account: RawTransactionArgument]; } /** - * The only way to finalize the TX is by sharing the account. All accounts are shared - * by default. + * The only way to finalize the TX is by sharing the account. All accounts are + * shared by default. */ export function share(options: ShareOptions) { const packageAddress = options.package ?? '@mysten/pas'; @@ -220,8 +220,8 @@ export interface UnsafeSendBalanceOptions { typeArguments: [string]; } /** - * Transfer `amount` from account to an address. This unlocks transfers to a account - * before it has been created. + * Transfer `amount` from account to an address. This unlocks transfers to a + * account before it has been created. * * It's marked as `unsafe_` as it's easy to accidentally pick the wrong recipient * address. @@ -260,7 +260,11 @@ export interface NewAuthAsObjectOptions { package?: string; arguments: NewAuthAsObjectArguments | [uid: RawTransactionArgument]; } -/** Generate an ownership proof from a `UID` object, to allow objects to own accounts. */ +/** + * Generate an ownership proof from a `UID` object, to allow objects to own + * accounts. `&mut UID` is intentional — it serves as proof of ownership over the + * object. + */ export function newAuthAsObject(options: NewAuthAsObjectOptions) { const packageAddress = options.package ?? '@mysten/pas'; const argumentsTypes = ['0x2::object::ID'] satisfies (string | null)[]; diff --git a/sdk/pas/src/contracts/pas/policy.ts b/sdk/pas/src/contracts/pas/policy.ts index 5a22afc..4458a07 100644 --- a/sdk/pas/src/contracts/pas/policy.ts +++ b/sdk/pas/src/contracts/pas/policy.ts @@ -197,7 +197,7 @@ export interface SyncVersioningOptions { } /** * Allows syncing the versioning of a policy to the namespace's versioning. This is - * permission-less and can be done + * permission-less and can be done by anyone. */ export function syncVersioning(options: SyncVersioningOptions) { const packageAddress = options.package ?? '@mysten/pas'; diff --git a/sdk/pas/src/contracts/pas/templates.ts b/sdk/pas/src/contracts/pas/templates.ts index 776da1f..035c88e 100644 --- a/sdk/pas/src/contracts/pas/templates.ts +++ b/sdk/pas/src/contracts/pas/templates.ts @@ -15,6 +15,12 @@ import { type Transaction } from '@mysten/sui/transactions'; import { MoveStruct, normalizeMoveArguments, type RawTransactionArgument } from '../utils/index.js'; const $moduleName = '@mysten/pas::templates'; +export const PAS = new MoveStruct({ + name: `${$moduleName}::PAS`, + fields: { + dummy_field: bcs.bool(), + }, +}); export const Templates = new MoveStruct({ name: `${$moduleName}::Templates`, fields: { @@ -28,6 +34,7 @@ export interface SetupOptions { package?: string; arguments: SetupArguments | [namespace: RawTransactionArgument]; } +/** Create the templates registry */ export function setup(options: SetupOptions) { const packageAddress = options.package ?? '@mysten/pas'; const argumentsTypes = [null] satisfies (string | null)[]; @@ -56,6 +63,7 @@ export interface SetTemplateCommandOptions { ]; typeArguments: [string]; } +/** Sets the PTB template for a given Action. */ export function setTemplateCommand(options: SetTemplateCommandOptions) { const packageAddress = options.package ?? '@mysten/pas'; const argumentsTypes = [null, null, null] satisfies (string | null)[]; diff --git a/sdk/pas/src/contracts/pas/unlock_funds.ts b/sdk/pas/src/contracts/pas/unlock_funds.ts index fcfdb8d..253a545 100644 --- a/sdk/pas/src/contracts/pas/unlock_funds.ts +++ b/sdk/pas/src/contracts/pas/unlock_funds.ts @@ -23,7 +23,7 @@ export function UnlockFunds>(...typeParameters: [T]) { return new MoveStruct({ name: `${$moduleName}::UnlockFunds<${typeParameters[0].name as T['name']}>`, fields: { - /** `from` is the wallet OR object address, NOT the account address */ + /** `owner` is the wallet OR object address, NOT the account address */ owner: bcs.Address, /** The ID of the account the funds are coming from */ account_id: bcs.Address, diff --git a/sdk/pas/src/contracts/ptb/ptb.ts b/sdk/pas/src/contracts/ptb/ptb.ts index 5d17d78..b09f95d 100644 --- a/sdk/pas/src/contracts/ptb/ptb.ts +++ b/sdk/pas/src/contracts/ptb/ptb.ts @@ -16,6 +16,16 @@ import { } from '../utils/index.js'; const $moduleName = '@mysten/ptb::ptb'; +export const Command = new MoveTuple({ + name: `${$moduleName}::Command`, + fields: [bcs.u8(), bcs.vector(bcs.u8())], +}); +export const Transaction = new MoveStruct({ + name: `${$moduleName}::Transaction`, + fields: { + commands: bcs.vector(Command), + }, +}); /** * Defines a simplified `ObjectArg` type for the `Transaction`. * @@ -86,25 +96,12 @@ export const CallArg = new MoveEnum({ /** * Extended arguments for off-chain resolution. Can be created and registered in a * transaction through `ext_input`. + * + * Extended arguments are namespaced by Type associated with them. In an + * application, this can be the root object, or a special type used for off chain + * resolution. */ - Ext: new MoveStruct({ - name: `CallArg.Ext`, - fields: { - namespace: bcs.string(), - value: bcs.string(), - }, - }), - }, -}); -export const Command = new MoveTuple({ - name: `${$moduleName}::Command`, - fields: [bcs.u8(), bcs.vector(bcs.u8())], -}); -export const Transaction = new MoveStruct({ - name: `${$moduleName}::Transaction`, - fields: { - inputs: bcs.vector(CallArg), - commands: bcs.vector(Command), + Ext: new MoveTuple({ name: `CallArg.Ext`, fields: [bcs.string(), bcs.string()] }), }, }); /** Defines a simplified `Argument` type for the `Transaction`. */ @@ -232,6 +229,48 @@ export function display(options: DisplayOptions = {}) { function: 'display', }); } +export interface DenyListOptions { + package?: string; + arguments?: []; +} +/** Shorthand for `object_by_id` with `0x403` (DenyList). */ +export function denyList(options: DenyListOptions = {}) { + const packageAddress = options.package ?? '@mysten/ptb'; + return (tx: Transaction_1) => + tx.moveCall({ + package: packageAddress, + module: 'ptb', + function: 'deny_list', + }); +} +export interface CoinRegistryOptions { + package?: string; + arguments?: []; +} +/** Shorthand for `object_by_id` with `0xC` (CoinRegistry). */ +export function coinRegistry(options: CoinRegistryOptions = {}) { + const packageAddress = options.package ?? '@mysten/ptb'; + return (tx: Transaction_1) => + tx.moveCall({ + package: packageAddress, + module: 'ptb', + function: 'coin_registry', + }); +} +export interface AccumulatorRootOptions { + package?: string; + arguments?: []; +} +/** Shorthand for `object_by_id` with `0xACC` (AccumulatorRoot). */ +export function accumulatorRoot(options: AccumulatorRootOptions = {}) { + const packageAddress = options.package ?? '@mysten/ptb'; + return (tx: Transaction_1) => + tx.moveCall({ + package: packageAddress, + module: 'ptb', + function: 'accumulator_root', + }); +} export interface GasOptions { package?: string; arguments?: []; @@ -449,6 +488,7 @@ export interface ExtInputArguments { export interface ExtInputOptions { package?: string; arguments: ExtInputArguments | [name: RawTransactionArgument]; + typeArguments: [string]; } /** * Create an external input handler. Expected to be understood by the off-chain @@ -464,6 +504,33 @@ export function extInput(options: ExtInputOptions) { module: 'ptb', function: 'ext_input', arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), + typeArguments: options.typeArguments, + }); +} +export interface ExtInputRawArguments { + namespace: RawTransactionArgument; + name: RawTransactionArgument; +} +export interface ExtInputRawOptions { + package?: string; + arguments: + | ExtInputRawArguments + | [namespace: RawTransactionArgument, name: RawTransactionArgument]; +} +/** + * Create an external input handler for a given type T. This can be used to + * hardcode the namespace value without having access to `T`. + */ +export function extInputRaw(options: ExtInputRawOptions) { + const packageAddress = options.package ?? '@mysten/ptb'; + const argumentsTypes = ['0x1::string::String', '0x1::string::String'] satisfies (string | null)[]; + const parameterNames = ['namespace', 'name']; + return (tx: Transaction_1) => + tx.moveCall({ + package: packageAddress, + module: 'ptb', + function: 'ext_input_raw', + arguments: normalizeMoveArguments(options.arguments, argumentsTypes, parameterNames), }); } export interface CommandArguments { diff --git a/sdk/pas/src/index.ts b/sdk/pas/src/index.ts index 03d64ea..2c76672 100644 --- a/sdk/pas/src/index.ts +++ b/sdk/pas/src/index.ts @@ -3,8 +3,5 @@ export { PASClient, pas } from './client.js'; export type { PASClientConfig, PASPackageConfig, PASOptions } from './types.js'; -export { - TESTNET_PAS_PACKAGE_CONFIG, - MAINNET_PAS_PACKAGE_CONFIG, -} from './constants.js'; +export { TESTNET_PAS_PACKAGE_CONFIG, MAINNET_PAS_PACKAGE_CONFIG } from './constants.js'; export * from './error.js'; diff --git a/sdk/pas/src/resolution.ts b/sdk/pas/src/resolution.ts index 46d6ccb..ffc4824 100644 --- a/sdk/pas/src/resolution.ts +++ b/sdk/pas/src/resolution.ts @@ -198,8 +198,8 @@ export function buildMoveCallCommandFromTemplate( } else if (arg.Input.Ext) { resolvedArgs.push( resolveRawPasRequest(args, { - _namespace: arg.Input.Ext.namespace, - value: arg.Input.Ext.value, + _namespace: arg.Input.Ext[0], + value: arg.Input.Ext[1], }), ); } else { diff --git a/sdk/pas/test/e2e/data/demo_usd/Move.lock b/sdk/pas/test/e2e/data/demo_usd/Move.lock index 381b097..ebf9a3b 100644 --- a/sdk/pas/test/e2e/data/demo_usd/Move.lock +++ b/sdk/pas/test/e2e/data/demo_usd/Move.lock @@ -5,13 +5,13 @@ version = 4 [pinned.testnet.MoveStdlib] -source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/move-stdlib", rev = "868c226359ef914f1f3b080518f27eb13d8967f5" } +source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/move-stdlib", rev = "b38bca86f0323b3fe8b6b7f4ca0cd7ae7faebe4b" } use_environment = "testnet" manifest_digest = "C4FE4C91DE74CBF223B2E380AE40F592177D21870DC2D7EB6227D2D694E05363" deps = {} [pinned.testnet.Sui] -source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "868c226359ef914f1f3b080518f27eb13d8967f5" } +source = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "b38bca86f0323b3fe8b6b7f4ca0cd7ae7faebe4b" } use_environment = "testnet" manifest_digest = "7AFB66695545775FBFBB2D3078ADFD084244D5002392E837FDE21D9EA1C6D01C" deps = { MoveStdlib = "MoveStdlib" } @@ -19,17 +19,17 @@ deps = { MoveStdlib = "MoveStdlib" } [pinned.testnet.demo_usd] source = { root = true } use_environment = "testnet" -manifest_digest = "F3F3BE825FCACCADB2ECE4ADCDD2DA4CD2C8D0DDAC32D4F23CBCE3F2760282C5" +manifest_digest = "31F68DEA541AB27E322EFF2DDC82ABB0893418A6E8B029F2FD0E0F80D7FE263B" deps = { pas = "pas", ptb = "ptb", std = "MoveStdlib", sui = "Sui" } [pinned.testnet.pas] -source = { local = "../pas" } +source = { git = "https://github.com/mystenlabs/pas.git", subdir = "packages/pas", rev = "b710e43a555bb66ec6d5288441a25b41c2d339f2" } use_environment = "testnet" manifest_digest = "38AA62656ABE7551C444DA427ADBAA7751CB67250663D39FCDE36E938138EA7D" deps = { ptb = "ptb", std = "MoveStdlib", sui = "Sui" } [pinned.testnet.ptb] -source = { local = "../ptb" } +source = { git = "https://github.com/mystenlabs/pas.git", subdir = "packages/ptb", rev = "b710e43a555bb66ec6d5288441a25b41c2d339f2" } use_environment = "testnet" manifest_digest = "5745706258F61D6CE210904B3E6AE87A73CE9D31A6F93BE4718C442529332A87" deps = { std = "MoveStdlib", sui = "Sui" } diff --git a/sdk/pas/test/e2e/data/demo_usd/Move.toml b/sdk/pas/test/e2e/data/demo_usd/Move.toml index ddb3332..dfc951b 100644 --- a/sdk/pas/test/e2e/data/demo_usd/Move.toml +++ b/sdk/pas/test/e2e/data/demo_usd/Move.toml @@ -3,5 +3,6 @@ name = "demo_usd" edition = "2024.beta" [dependencies] -pas = { local = "../pas" } -ptb = { local = "../ptb" } +# testnet v1 +pas = { git = "https://github.com/mystenlabs/pas.git", subdir = "packages/pas", rev = "b710e43a555bb66ec6d5288441a25b41c2d339f2" } +ptb = { git = "https://github.com/mystenlabs/pas.git", subdir = "packages/ptb", rev = "b710e43a555bb66ec6d5288441a25b41c2d339f2" } diff --git a/sdk/pas/test/e2e/e2e.isolated.test.ts b/sdk/pas/test/e2e/e2e.isolated.test.ts index 3a58dc5..03a0261 100644 --- a/sdk/pas/test/e2e/e2e.isolated.test.ts +++ b/sdk/pas/test/e2e/e2e.isolated.test.ts @@ -64,7 +64,7 @@ describe.concurrent( // try to do an unlock but it should fail because `policy` for Sui does not exist. const tx = new Transaction(); tx.add( - toolbox.client.pas.tx.unlockBalance({ + toolbox.client.pas.call.unlockBalance({ from: toolbox.address(), amount: 1_000_000_000, assetType: suiTypeName, @@ -78,7 +78,7 @@ describe.concurrent( // Now let's unlock funds properly. const unlockTx = new Transaction(); const withdrawal = unlockTx.add( - toolbox.client.pas.tx.unlockUnrestrictedBalance({ + toolbox.client.pas.call.unlockUnrestrictedBalance({ from: toolbox.address(), amount: 1_000_000_000, assetType: suiTypeName, @@ -125,7 +125,7 @@ describe.concurrent( const tx = new Transaction(); tx.add( - toolbox.client.pas.tx.sendBalance({ + toolbox.client.pas.call.sendBalance({ from, to, amount: 100 * 1_000_000, @@ -166,7 +166,7 @@ describe.concurrent( const transaction = new Transaction(); transaction.add( - toolbox.client.pas.tx.sendBalance({ + toolbox.client.pas.call.sendBalance({ from, to, amount: 1_000_000, @@ -217,7 +217,7 @@ describe.concurrent( const tx = new Transaction(); // (1) accountForAddress for sender -- forces implicit creation - tx.add(toolbox.client.pas.tx.accountForAddress(sender)); + tx.add(toolbox.client.pas.call.accountForAddress(sender)); // (2) Intermediate command: a harmless moveCall (merge empty split back into gas) const split1 = tx.splitCoins(tx.gas, [tx.pure.u64(0)]); @@ -225,7 +225,7 @@ describe.concurrent( // (3) First transfer: sender -> receiver (receiver account does not exist) tx.add( - toolbox.client.pas.tx.sendBalance({ + toolbox.client.pas.call.sendBalance({ from: sender, to: receiver, amount: 50 * 1_000_000, @@ -239,7 +239,7 @@ describe.concurrent( // (5) Second transfer: sender -> receiver (both accounts already created in this PTB) tx.add( - toolbox.client.pas.tx.sendBalance({ + toolbox.client.pas.call.sendBalance({ from: sender, to: receiver, amount: 50 * 1_000_000, @@ -282,7 +282,7 @@ describe.concurrent( const tx = new Transaction(); tx.add( - toolbox.client.pas.tx.sendBalance({ + toolbox.client.pas.call.sendBalance({ from, to, amount: 15_000 * 1_000_000, @@ -310,7 +310,7 @@ describe.concurrent( const tx = new Transaction(); tx.add( - toolbox.client.pas.tx.sendBalance({ + toolbox.client.pas.call.sendBalance({ from: addr, to: addr, amount: 1_000_000, @@ -340,7 +340,7 @@ describe.concurrent( const transaction = new Transaction(); transaction.add( - toolbox.client.pas.tx.sendBalance({ + toolbox.client.pas.call.sendBalance({ from, to, amount: 100 * 1_000_000, @@ -370,7 +370,7 @@ describe.concurrent( const tx = new Transaction(); tx.add( - toolbox.client.pas.tx.sendBalance({ + toolbox.client.pas.call.sendBalance({ from, to, amount: 15_000 * 1_000_000, @@ -407,7 +407,7 @@ describe.concurrent( // --- First PTB: transfers both asset types, implicitly creates receiver account --- const tx1 = new Transaction(); tx1.add( - toolbox.client.pas.tx.sendBalance({ + toolbox.client.pas.call.sendBalance({ from: sender, to: receiver, amount: 120 * 1_000_000, @@ -415,7 +415,7 @@ describe.concurrent( }), ); tx1.add( - toolbox.client.pas.tx.sendBalance({ + toolbox.client.pas.call.sendBalance({ from: sender, to: receiver, amount: 350 * 1_000_000, @@ -436,7 +436,7 @@ describe.concurrent( // --- Second PTB: both accounts already exist, different amounts --- const tx2 = new Transaction(); tx2.add( - toolbox.client.pas.tx.sendBalance({ + toolbox.client.pas.call.sendBalance({ from: sender, to: receiver, amount: 80 * 1_000_000, @@ -444,7 +444,7 @@ describe.concurrent( }), ); tx2.add( - toolbox.client.pas.tx.sendBalance({ + toolbox.client.pas.call.sendBalance({ from: sender, to: receiver, amount: 150 * 1_000_000, @@ -478,7 +478,7 @@ describe.concurrent( const tx = new Transaction(); tx.add( - toolbox.client.pas.tx.sendBalance({ + toolbox.client.pas.call.sendBalance({ from, to, amount: 1_000_000, diff --git a/sdk/pas/test/e2e/e2e.shared.test.ts b/sdk/pas/test/e2e/e2e.shared.test.ts index a7d8f2d..ebf9fc7 100644 --- a/sdk/pas/test/e2e/e2e.shared.test.ts +++ b/sdk/pas/test/e2e/e2e.shared.test.ts @@ -28,7 +28,7 @@ describe('e2e tests with shared PAS package (all tests run in the same PAS packa const tx = new Transaction(); tx.add( - toolbox.client.pas.tx.unlockBalance({ + toolbox.client.pas.call.unlockBalance({ from: address, amount: 100 * 1_000_000, assetType: demoUsd.demoUsdAssetType, diff --git a/sdk/pas/test/e2e/globalSetup.ts b/sdk/pas/test/e2e/globalSetup.ts index f5094c6..785f92d 100644 --- a/sdk/pas/test/e2e/globalSetup.ts +++ b/sdk/pas/test/e2e/globalSetup.ts @@ -45,11 +45,7 @@ export default async function setup(project: TestProject) { '--with-graphql', `--with-indexer=postgres://postgres:postgrespw@${pg.getIpAddress(network.getName())}:5432/sui_indexer_v2`, ]) - .withCopyDirectoriesToContainer([ - { source: resolve(__dirname, '../../../../packages/pas'), target: '/test-data/pas' }, - { source: resolve(__dirname, '../../../../packages/ptb'), target: '/test-data/ptb' }, - { source: resolve(__dirname, 'data'), target: '/test-data' }, - ]) + .withCopyDirectoriesToContainer([{ source: resolve(__dirname, 'data'), target: '/test-data' }]) .withNetwork(network) .withExposedPorts(9000, 9123, 9124, 9125) .withLogConsumer((stream) => { diff --git a/sdk/pas/test/e2e/setup.ts b/sdk/pas/test/e2e/setup.ts index 4ef78c1..fc8e091 100644 --- a/sdk/pas/test/e2e/setup.ts +++ b/sdk/pas/test/e2e/setup.ts @@ -4,6 +4,7 @@ import path from 'path'; import type { ClientWithExtensions } from '@mysten/sui/client'; import { FaucetRateLimitError, getFaucetHost, requestSuiFromFaucetV2 } from '@mysten/sui/faucet'; +import { SuiGraphQLClient } from '@mysten/sui/graphql'; import { SuiGrpcClient } from '@mysten/sui/grpc'; import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519'; import { Transaction } from '@mysten/sui/transactions'; @@ -17,6 +18,7 @@ import { pas, type PASClient } from '../../src/index.js'; const DEFAULT_FAUCET_URL = process.env.FAUCET_URL ?? getFaucetHost('localnet'); const DEFAULT_FULLNODE_URL = process.env.FULLNODE_URL ?? 'http://127.0.0.1:9000'; +const DEFAULT_GRAPHQL_URL = process.env.GRAPHQL_URL ?? 'http://127.0.0.1:9125/graphql'; export type PASClientType = ClientWithExtensions<{ pas: PASClient }, SuiGrpcClient>; @@ -110,7 +112,7 @@ export class TestToolbox { // Creates a account for a given address. async createAccountForAddress(address: string) { const tx = new Transaction(); - tx.add(this.client.pas.tx.accountForAddress(address)); + tx.add(this.client.pas.call.accountForAddress(address)); return this.executeTransaction(tx); } @@ -148,55 +150,48 @@ export async function setupToolbox() { const configPath = path.join(configDir, 'client.yaml'); await execSuiTools(['sui', 'client', '--yes', '--client.config', configPath]); - // Create a pub file that's persistent per test run. const pubFilePath = path.join(configDir, 'publications.toml'); - // Switch CLI to local env. await execSuiTools(['sui', 'client', '--client.config', configPath, 'switch', '--env', 'local']); - - // Get some gas for any publishes. await execSuiTools(['sui', 'client', '--client.config', configPath, 'faucet']); - // Track the published packages. const publishedPackages: Record = {}; - // publish PTB package - const ptbPublishData = await publishPackage('ptb', { + // Publish demo_usd with --publish-unpublished-deps so that pas and ptb + // are transitively published without needing local copies of those packages. + const demoUsdData = await publishPackage('demo_usd', { configPath, pubFilePath, baseClient, }); - publishedPackages.ptb = { - digest: ptbPublishData.digest, - createdObjects: ptbPublishData.createdObjects, - originalId: ptbPublishData.packageId, - publishedAt: ptbPublishData.packageId, + publishedPackages.demo_usd = { + digest: demoUsdData.digest, + createdObjects: demoUsdData.createdObjects, + originalId: demoUsdData.packageId, + publishedAt: demoUsdData.packageId, }; - // publish PAS package - const pasPublishData = await publishPackage('pas', { - configPath, - pubFilePath, - baseClient, - }); + // Discover the transitively-published pas package by querying the CLI + // sender's recent transactions via GraphQL. + const cliAddress = await getCliAddress(configPath); + const pasPublish = await discoverPasPackage(cliAddress, demoUsdData.digest); publishedPackages.pas = { - digest: pasPublishData.digest, - createdObjects: pasPublishData.createdObjects, - originalId: pasPublishData.packageId, - publishedAt: pasPublishData.packageId, + digest: pasPublish.digest, + createdObjects: pasPublish.createdObjects, + originalId: pasPublish.packageId, + publishedAt: pasPublish.packageId, }; - const pasPackageId = pasPublishData.packageId; - const namespaceId = pasPublishData.createdObjects.find((obj) => + const pasPackageId = pasPublish.packageId; + const namespaceId = pasPublish.createdObjects.find((obj) => obj.type.endsWith('namespace::Namespace'), )?.id!; - const upgradeCapId = pasPublishData.createdObjects.find((obj) => + const upgradeCapId = pasPublish.createdObjects.find((obj) => obj.type.endsWith('UpgradeCap'), )?.id!; - // Extend the client with pas so we can use it across our testing. const client = baseClient.$extend( pas({ packageConfig: { @@ -208,7 +203,6 @@ export async function setupToolbox() { // Link the UpgradeCap to the Namespace (required before any derived object operations). // This must be done via CLI since the UpgradeCap is owned by the CLI address, not the test keypair. - await execSuiTools([ 'sui', 'client', @@ -227,23 +221,131 @@ export async function setupToolbox() { return new TestToolbox(keypair, client, configPath, pubFilePath, publishedPackages); } -// Extend client with PAS once we have the package/namespace IDs. -export function extendWithPAS(toolbox: TestToolbox, packageId: string, namespaceId: string): void { - const extendedClient = (toolbox.client as unknown as SuiGrpcClient).$extend( - pas({ - packageConfig: { - packageId, - namespaceId, - }, - }), - ); - toolbox.client = extendedClient as PASClientType; +async function getCliAddress(configPath: string): Promise { + const result = await execSuiTools([ + 'sui', + 'client', + '--client.config', + configPath, + 'active-address', + ]); + return normalizeSuiAddress(result.stdout.trim()); +} + +const DISCOVER_PAS_QUERY = ` +query ($sender: String!) { + address(address: $sender) { + transactions(relation: SENT, last: 10) { + nodes { + digest + effects { + objectChanges(first: 50) { + nodes { + address + idCreated + outputState { + asMovePackage { + address + } + asMoveObject { + contents { + type { repr } + } + } + } + } + } + } + } + } + } } +`; + +type GqlObjectChangeNode = { + address: string; + idCreated: boolean | null; + outputState: { + asMovePackage: { address: string } | null; + asMoveObject: { contents: { type: { repr: string } } | null } | null; + } | null; +}; + +type GqlTransactionNode = { + digest: string; + effects: { + objectChanges: { + nodes: GqlObjectChangeNode[]; + }; + }; +}; -// This should be kept private as there's a risk of equivocating the -// CLI address if trying to publish from different executions in parallel. -// It's recommended that we only do the test publishes once in the beginning. -// Locking is now handled at the TestToolbox.publishPackage level. +/** + * After publishing demo_usd with --publish-unpublished-deps, the CLI sender + * will have executed separate transactions for each transitive dependency. + * This function queries those transactions via GraphQL and identifies the one + * that published the `pas` package (by looking for a created Namespace object). + */ +async function discoverPasPackage( + senderAddress: string, + excludeDigest: string, +): Promise<{ digest: string; packageId: string; createdObjects: { id: string; type: string }[] }> { + const graphqlClient = new SuiGraphQLClient({ + url: DEFAULT_GRAPHQL_URL, + network: 'localnet', + }); + + // The indexer may lag behind the fullnode, so retry until the transactions appear. + const result = await retry( + async () => { + const { data, errors } = await graphqlClient.query({ + query: DISCOVER_PAS_QUERY, + variables: { sender: senderAddress }, + }); + + if (errors?.length) throw new Error(`GraphQL errors: ${JSON.stringify(errors)}`); + const txNodes = (data as any)?.address?.transactions?.nodes as + | GqlTransactionNode[] + | undefined; + if (!txNodes?.length) throw new Error('No transactions found for CLI sender'); + + // Find the transaction that created a Namespace object (that's the pas publish). + for (const tx of txNodes) { + if (tx.digest === excludeDigest) continue; + + const changes = tx.effects.objectChanges.nodes; + const hasNamespace = changes.some( + (c) => + c.idCreated && + c.outputState?.asMoveObject?.contents?.type?.repr?.includes('namespace::Namespace'), + ); + if (!hasNamespace) continue; + + const packageId = changes.find((c) => c.idCreated && c.outputState?.asMovePackage) + ?.outputState?.asMovePackage?.address; + if (!packageId) continue; + + const createdObjects = changes + .filter((c) => c.idCreated && c.outputState?.asMoveObject) + .map((c) => ({ + id: c.address, + type: c.outputState!.asMoveObject!.contents!.type.repr, + })); + + return { digest: tx.digest, packageId, createdObjects }; + } + + throw new Error('Could not find pas publish transaction among sender transactions'); + }, + { + backoff: 'EXPONENTIAL', + timeout: 1000 * 60, + logger: (msg) => console.warn('Retrying pas package discovery: ' + msg), + }, + ); + + return result; +} async function publishPackage( packageName: string, @@ -257,8 +359,6 @@ async function publishPackage( baseClient: SuiGrpcClient; }, ) { - // Let's publish using `test-publish` command. - // Should be reusing pubFilePaths for each package (so they depend on the same thing!). const result = await execSuiTools([ 'sui', 'client', @@ -267,6 +367,7 @@ async function publishPackage( 'test-publish', '--build-env', 'testnet', + '--publish-unpublished-deps', '--pubfile-path', pubFilePath, `/test-data/${packageName}`, @@ -276,7 +377,6 @@ async function publishPackage( // trim everything before `{` const resultJson = result.stdout.substring(result.stdout.indexOf('{')); const publicationDigest = JSON.parse(resultJson).digest; - // const transaction = await getCli // Get the TX to extract the package ID. const transaction = await baseClient.getTransaction({ @@ -364,6 +464,7 @@ export async function simulateTransaction(toolbox: TestToolbox, tx: Transaction) }); } +// @ts-ignore-next-line const SUI_TOOLS_CONTAINER_ID = inject('suiToolsContainerId'); export async function execSuiTools( diff --git a/sdk/pas/test/e2e/setupEnv.ts b/sdk/pas/test/e2e/setupEnv.ts index c1c8822..eed666c 100644 --- a/sdk/pas/test/e2e/setupEnv.ts +++ b/sdk/pas/test/e2e/setupEnv.ts @@ -1,6 +1,8 @@ // Copyright (c) Mysten Labs, Inc. // SPDX-License-Identifier: Apache-2.0 +// @ts-ignore + import { inject } from 'vitest'; Object.entries({ @@ -8,5 +10,6 @@ Object.entries({ FULLNODE_URL: `http://localhost:${inject('localnetPort')}`, GRAPHQL_URL: `http://localhost:${inject('graphqlPort')}/graphql`, }).forEach(([key, value]) => { + // @ts-ignore-next-line process.env[key] = value; });