@mysten/sui
Version:
Sui TypeScript API(Work in Progress)
186 lines (156 loc) • 5.17 kB
text/typescript
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0
import { parseStructTag } from '../../utils/sui-types.js';
import type { BuildTransactionOptions } from '../json-rpc-resolver.js';
import type { TransactionDataBuilder } from '../TransactionData.js';
import type { NamedPackagesPluginCache } from './utils.js';
import {
batch,
findNamesInTransaction,
getFirstLevelNamedTypes,
populateNamedTypesFromCache,
replaceNames,
} from './utils.js';
export type NamedPackagesPluginOptions = {
/**
* The URL of the MVR API to use for resolving names.
*/
url: string;
/**
* The number of names to resolve in each batch request.
* Needs to be calculated based on the GraphQL query limits.
*/
pageSize?: number;
/**
* Local overrides for the resolution plugin. Pass this to pre-populate
* the cache with known packages / types (especially useful for local or CI testing).
*
* The type cache expects ONLY first-level types to ensure the cache is more composable.
*
* Expected format example:
* {
* packages: {
* '@framework/std': '0x1234',
* },
* types: {
* '@framework/std::string::String': '0x1234::string::String',
* },
* }
*
*/
overrides?: NamedPackagesPluginCache;
};
/**
* @experimental This plugin is in experimental phase and there might be breaking changes in the future
*
* Adds named resolution so that you can use .move names in your transactions.
* e.g. `@org/app::type::Type` will be resolved to `0x1234::type::Type`.
* This plugin will resolve all names & types in the transaction block.
*
* To install this plugin globally in your app, use:
* ```
* Transaction.registerGlobalSerializationPlugin("namedPackagesPlugin", namedPackagesPlugin({ suiGraphQLClient }));
* ```
*
* You can also define `overrides` to pre-populate name resolutions locally (removes the GraphQL request).
*/
export const namedPackagesPlugin = ({
url,
pageSize = 50,
overrides = { packages: {}, types: {} },
}: NamedPackagesPluginOptions) => {
// validate that types are first-level only.
Object.keys(overrides.types).forEach((type) => {
if (parseStructTag(type).typeParams.length > 0)
throw new Error(
'Type overrides must be first-level only. If you want to supply generic types, just pass each type individually.',
);
});
const cache = overrides;
return async (
transactionData: TransactionDataBuilder,
_buildOptions: BuildTransactionOptions,
next: () => Promise<void>,
) => {
const names = findNamesInTransaction(transactionData);
const [packages, types] = await Promise.all([
resolvePackages(
names.packages.filter((x) => !cache.packages[x]),
url,
pageSize,
),
resolveTypes(
[...getFirstLevelNamedTypes(names.types)].filter((x) => !cache.types[x]),
url,
pageSize,
),
]);
// save first-level mappings to cache.
Object.assign(cache.packages, packages);
Object.assign(cache.types, types);
const composedTypes = populateNamedTypesFromCache(names.types, cache.types);
// when replacing names, we also need to replace the "composed" types collected above.
replaceNames(transactionData, {
packages: { ...cache.packages },
// we include the "composed" type cache too.
types: composedTypes,
});
await next();
};
async function resolvePackages(packages: string[], apiUrl: string, pageSize: number) {
if (packages.length === 0) return {};
const batches = batch(packages, pageSize);
const results: Record<string, string> = {};
await Promise.all(
batches.map(async (batch) => {
const response = await fetch(`${apiUrl}/v1/resolution/bulk`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
names: batch,
}),
});
if (!response.ok) {
const errorBody = await response.json().catch(() => ({}));
throw new Error(`Failed to resolve packages: ${errorBody?.message}`);
}
const data = await response.json();
if (!data?.resolution) return;
for (const pkg of Object.keys(data?.resolution)) {
const pkgData = data.resolution[pkg]?.package_id;
if (!pkgData) continue;
results[pkg] = pkgData;
}
}),
);
return results;
}
async function resolveTypes(types: string[], apiUrl: string, pageSize: number) {
if (types.length === 0) return {};
const batches = batch(types, pageSize);
const results: Record<string, string> = {};
await Promise.all(
batches.map(async (batch) => {
const response = await fetch(`${apiUrl}/v1/struct-definition/bulk`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
types: batch,
}),
});
if (!response.ok) {
const errorBody = await response.json().catch(() => ({}));
throw new Error(`Failed to resolve types: ${errorBody?.message}`);
}
const data = await response.json();
if (!data?.resolution) return;
for (const type of Object.keys(data?.resolution)) {
const typeData = data.resolution[type]?.type_tag;
if (!typeData) continue;
results[type] = typeData;
}
}),
);
return results;
}
};