diff --git a/examples/typescript/keyless.ts b/examples/typescript/keyless.ts index 80fab0ef0..4d993eaff 100644 --- a/examples/typescript/keyless.ts +++ b/examples/typescript/keyless.ts @@ -119,7 +119,6 @@ const example = async () => { example(); - // const testPermissioned = () => { // const subAccount = AbstractedEd25519Account.generate(); diff --git a/src/api/permissions.ts b/src/api/permissions.ts index d1ef448c6..09d3400b3 100644 --- a/src/api/permissions.ts +++ b/src/api/permissions.ts @@ -4,7 +4,7 @@ import { getPermissions, requestPermission, revokePermissions } from "../internal/permissions"; import { AptosConfig } from "./aptosConfig"; import { SimpleTransaction } from "../transactions/instances/simpleTransaction"; -import { FilteredPermissions, Permission, PermissionType, RevokePermission } from "../types/permissions"; +import { Permission } from "../types/permissions"; import { AccountAddress, Ed25519PublicKey } from "../core"; /** @@ -32,12 +32,11 @@ export class Permissions { * * @returns A promise that resolves to an array of current permissions and their remaining balances. */ - async getPermissions(args: { + async getPermissions(args: { primaryAccountAddress: AccountAddress; subAccountPublicKey: Ed25519PublicKey; - - filter?: T; - }): Promise> { + filter?: new (...a: any) => T; + }): Promise { return getPermissions({ aptosConfig: this.config, primaryAccountAddress: args.primaryAccountAddress, @@ -111,7 +110,7 @@ export class Permissions { async revokePermission(args: { primaryAccountAddress: AccountAddress; subAccountPublicKey: Ed25519PublicKey; - permissions: RevokePermission[]; + permissions: Permission[]; }): Promise { return revokePermissions({ aptosConfig: this.config, diff --git a/src/index.ts b/src/index.ts index 0e5041546..51b038ad0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,4 +9,5 @@ export * from "./core"; export * from "./transactions"; export * from "./transactions/management"; export * from "./types"; +export * from "./types/permissions"; export * from "./utils"; diff --git a/src/internal/permissions.ts b/src/internal/permissions.ts index aba49fc4c..a1b51f19c 100644 --- a/src/internal/permissions.ts +++ b/src/internal/permissions.ts @@ -14,20 +14,12 @@ import { AccountAddress, Ed25519PublicKey } from "../core"; import { SimpleTransaction } from "../transactions/instances/simpleTransaction"; import { MoveString } from "../bcs"; import { AptosIntentBuilder } from "../transactions"; -import { - FilteredPermissions, - PermissionType, - MoveVMPermissionType, - RevokePermission, - Permission, - buildFungibleAssetPermission, - buildNFTPermission, -} from "../types/permissions"; +import { MoveVMPermissionType, Permission, FungibleAssetPermission, NFTPermission } from "../types/permissions"; import { Transaction } from "../api/transaction"; import { view } from "./view"; // functions -export async function getPermissions({ +export async function getPermissions({ aptosConfig, primaryAccountAddress, subAccountPublicKey, @@ -36,8 +28,8 @@ export async function getPermissions({ aptosConfig: AptosConfig; primaryAccountAddress: AccountAddress; subAccountPublicKey: Ed25519PublicKey; - filter?: T; -}): Promise> { + filter?: new (...a: any) => T; +}): Promise { const handle = await getHandleAddress({ aptosConfig, primaryAccountAddress, subAccountPublicKey }); const res = await fetch(`${aptosConfig.fullnode}/accounts/${handle}/resources`); @@ -59,13 +51,13 @@ export async function getPermissions({ const permissions = data[0].data.perms.data.map((d) => { switch (d.key.type_name) { case MoveVMPermissionType.FungibleAsset: // can this be better? i dont rly like this - return buildFungibleAssetPermission({ - asset: d.key.data, + return FungibleAssetPermission.from({ + asset: AccountAddress.fromString(d.key.data), balance: d.value, }); case MoveVMPermissionType.TransferPermission: - return buildNFTPermission({ - assetAddress: d.key.data, + return NFTPermission.from({ + assetAddress: AccountAddress.fromString(d.key.data), capabilities: { transfer: true, mutate: false }, }); default: @@ -74,8 +66,8 @@ export async function getPermissions({ } }); - const filtered = filter ? permissions.filter((p) => filter.includes(p.type as PermissionType)) : permissions; - return filtered as FilteredPermissions; + const filtered = filter ? permissions.filter((p) => p instanceof filter) : permissions; + return filtered as T[]; } // should it return the requested permissions? on success? and when it fails it @@ -108,21 +100,24 @@ export async function requestPermission(args: { // if nft permission has multiple capabilities, we need to add multiple txns // For NFT permissions with multiple capabilities, split into separate transactions - const expandedPermissions = permissions.flatMap((permission) => { - if (permission.type === PermissionType.NFT && permission.capabilities) { - const expanded = []; + if (permission instanceof NFTPermission && permission.capabilities) { + const expanded: Permission[] = []; if (permission.capabilities.transfer) { - expanded.push({ - ...permission, - capabilities: { transfer: true, mutate: false }, - }); + expanded.push( + NFTPermission.from({ + assetAddress: permission.assetAddress, + capabilities: { transfer: true, mutate: false }, + }), + ); } if (permission.capabilities.mutate) { - expanded.push({ - ...permission, - capabilities: { transfer: false, mutate: true }, - }); + expanded.push( + NFTPermission.from({ + assetAddress: permission.assetAddress, + capabilities: { transfer: false, mutate: true }, + }), + ); } return expanded; } @@ -157,7 +152,7 @@ export async function revokePermissions(args: { aptosConfig: AptosConfig; primaryAccountAddress: AccountAddress; subAccountPublicKey: Ed25519PublicKey; - permissions: RevokePermission[]; + permissions: Permission[]; }): Promise { const { aptosConfig, primaryAccountAddress, subAccountPublicKey, permissions } = args; @@ -172,27 +167,24 @@ export async function revokePermissions(args: { }); const permissionPromises = permissions.map((permission) => { - switch (permission.type) { - case PermissionType.FungibleAsset: { - return builder.add_batched_calls({ - function: "0x1::fungible_asset::revoke_permission", - functionArguments: [signer[0].borrow(), permission.asset], - typeArguments: [], - }); - } - // TODO: object nft revoke - case PermissionType.NFT: { - return builder.add_batched_calls({ - function: "0x1::object::revoke_permission", - functionArguments: [signer[0].borrow(), permission.assetAddress], - typeArguments: ["0x4::token::Token"], - }); - } - default: { - console.log("Not implemented"); - return Promise.resolve(); - } + if (permission instanceof FungibleAssetPermission) { + return builder.add_batched_calls({ + function: "0x1::fungible_asset::revoke_permission", + functionArguments: [signer[0].borrow(), permission.asset], + typeArguments: [], + }); + } + // TODO: object nft revoke + if (permission instanceof NFTPermission) { + return builder.add_batched_calls({ + function: "0x1::object::revoke_permission", + functionArguments: [signer[0].borrow(), permission.assetAddress], + typeArguments: ["0x4::token::Token"], + }); } + + console.log("Not implemented"); + return Promise.resolve(); }); await Promise.all(permissionPromises); @@ -241,42 +233,41 @@ async function grantPermission( permission: Permission; }, ) { - switch (args.permission.type) { - case PermissionType.FungibleAsset: + if (args.permission instanceof FungibleAssetPermission) { + return builder.add_batched_calls({ + function: "0x1::fungible_asset::grant_permission", + functionArguments: [ + BatchArgument.new_signer(0), + args.permissionedSigner[0].borrow(), + args.permission.asset, // do i need to convert this to AccountAddress? .... i guess not?? + args.permission.balance, + ], + typeArguments: [], + }); + } + if (args.permission instanceof NFTPermission) { + const txn: Promise[] = []; + if (args.permission.capabilities.transfer) { return builder.add_batched_calls({ - function: "0x1::fungible_asset::grant_permission", + function: "0x1::object::grant_permission", functionArguments: [ BatchArgument.new_signer(0), args.permissionedSigner[0].borrow(), - args.permission.asset, // do i need to convert this to AccountAddress? .... i guess not?? - args.permission.balance, + args.permission.assetAddress, ], - typeArguments: [], + typeArguments: ["0x4::token::Token"], }); - case PermissionType.NFT: { - const txn: Promise[] = []; - if (args.permission.capabilities.transfer) { - return builder.add_batched_calls({ - function: "0x1::object::grant_permission", - functionArguments: [ - BatchArgument.new_signer(0), - args.permissionedSigner[0].borrow(), - args.permission.assetAddress, - ], - typeArguments: ["0x4::token::Token"], - }); - } - if (args.permission.capabilities.mutate) { - console.log("mutate not implemented"); - throw new Error("mutate not implemented"); - } - return txn; } - default: - console.log("Not implemented"); - throw new Error(`${args.permission.type} not implemented`); - return Promise.resolve(); + if (args.permission.capabilities.mutate) { + console.log("mutate not implemented"); + throw new Error("mutate not implemented"); + } + return txn; } + + console.log("Not implemented"); + throw new Error(`${args.permission} not implemented`); + return Promise.resolve(); } async function finalizeNewHandle( diff --git a/src/types/permissions.ts b/src/types/permissions.ts index 81da8b437..0eab49332 100644 --- a/src/types/permissions.ts +++ b/src/types/permissions.ts @@ -4,12 +4,20 @@ * along with interfaces and factory functions for creating and revoking permissions. */ +import type { Deserializer } from "../bcs/deserializer"; +import type { Serializer } from "../bcs/serializer"; +import { Serializable } from "../bcs/serializer"; +import { AccountAddress } from "../core/accountAddress"; + /** * Core permission type definitions */ export type Permission = FungibleAssetPermission | GasPermission | NFTPermission | NFTCollectionPermission; -export type RevokePermission = RevokeFungibleAssetPermission | RevokeNFTPermission | Permission; -export type FilteredPermissions = Array>; + +export enum MoveVMPermissionType { + FungibleAsset = "0x1::fungible_asset::WithdrawPermission", + TransferPermission = "0x1::object::TransferPermission", +} /** * Permission handle metadata and configuration @@ -24,88 +32,147 @@ export interface PermissionHandle { permissions: Permission[]; } -/** - * Capability and permission type enums - */ -export enum NFTCapability { - transfer = "transfer", - mutate = "mutate", -} +export class FungibleAssetPermission extends Serializable { + readonly asset: AccountAddress; -export enum PermissionType { - FungibleAsset = "FungibleAsset", - Gas = "Gas", - NFT = "NFT", - NFTCollection = "Collection", -} + readonly balance: string; -/** - * Permission interfaces for different asset types - */ -export interface FungibleAssetPermission { - type: PermissionType.FungibleAsset; - asset: string; - // Question: best type here?: number | string | bigint - balance: string; -} + constructor({ asset, balance }: { asset: AccountAddress; balance: string }) { + super(); + this.asset = asset; + this.balance = balance; + } -export interface GasPermission { - type: PermissionType.Gas; - amount: number; -} + static from(args: { asset: AccountAddress; balance: string }): FungibleAssetPermission { + return new FungibleAssetPermission(args); + } -export interface NFTPermission { - type: PermissionType.NFT; - assetAddress: string; - capabilities: Record; + serialize(serializer: Serializer): void { + this.asset.serialize(serializer); + serializer.serializeStr(this.balance); + } + + static deserialize(deserializer: Deserializer): FungibleAssetPermission { + const asset = AccountAddress.deserialize(deserializer); + const balance = deserializer.deserializeStr(); + return FungibleAssetPermission.from({ asset, balance }); + } } -export interface NFTCollectionPermission { - type: PermissionType.NFTCollection; - collectionAddress: string; - capabilities: Record; +export class GasPermission extends Serializable { + readonly amount: number; + + constructor({ amount }: { amount: number }) { + super(); + this.amount = amount; + } + + static from = (args: { amount: number }): GasPermission => new GasPermission(args); + + serialize(serializer: Serializer): void { + serializer.serializeU16(this.amount); + } + + static deserialize(deserializer: Deserializer): GasPermission { + const amount = deserializer.deserializeU16(); + return GasPermission.from({ amount }); + } } -export enum NFTCollectionCapability { +enum NFTCapability { transfer = "transfer", mutate = "mutate", } -/** - * Revoke permission types - */ -export type RevokeFungibleAssetPermission = Pick; -export type RevokeNFTPermission = Pick; +export class NFTPermission extends Serializable { + readonly assetAddress: AccountAddress; -export enum MoveVMPermissionType { - FungibleAsset = "0x1::fungible_asset::WithdrawPermission", - TransferPermission = "0x1::object::TransferPermission", -} + readonly capabilities: Record; -/** - * Factory functions for creating permissions - */ -export function buildFungibleAssetPermission(args: Omit): FungibleAssetPermission { - return { type: PermissionType.FungibleAsset, ...args }; -} -export function buildGasPermission(args: Omit): GasPermission { - return { type: PermissionType.Gas, ...args }; -} -export function buildNFTPermission(args: Omit): NFTPermission { - return { type: PermissionType.NFT, ...args }; -} -export function buildNFTCollectionPermission(args: Omit): NFTCollectionPermission { - return { type: PermissionType.NFTCollection, ...args }; + constructor({ + assetAddress, + capabilities, + }: { + assetAddress: AccountAddress; + capabilities: Record; + }) { + super(); + this.assetAddress = assetAddress; + this.capabilities = capabilities; + } + + static from = (args: { assetAddress: AccountAddress; capabilities: Record }): NFTPermission => + new NFTPermission(args); + + serialize(serializer: Serializer): void { + this.assetAddress.serialize(serializer); + + const [capabilityKeys, capabilityValues] = Object.entries(this.capabilities).reduce( + ([keys, values], [key, value]) => [keys.concat(key), values.concat(value)], + [[] as string[], [] as boolean[]], + ); + serializer.serializeStr(JSON.stringify(capabilityKeys)); + serializer.serializeStr(JSON.stringify(capabilityValues)); + } + + static deserialize(deserializer: Deserializer): NFTPermission { + const assetAddress = AccountAddress.deserialize(deserializer); + const capabilityKeys = JSON.parse(deserializer.deserializeStr()) as string[]; + const capabilityValues = JSON.parse(deserializer.deserializeStr()) as boolean[]; + const capabilities = capabilityKeys.reduce( + (acc, key, i) => ({ ...acc, [key]: capabilityValues[i] }), + {} as Record, + ); + return NFTPermission.from({ assetAddress, capabilities }); + } } -/** - * Factory functions for creating revoke permissions - */ -export function buildRevokeFungibleAssetPermission( - args: Omit, -): RevokeFungibleAssetPermission { - return { type: PermissionType.FungibleAsset, ...args }; +enum NFTCollectionCapability { + transfer = "transfer", + mutate = "mutate", } -export function buildRevokeNFTPermission(args: Omit): RevokeNFTPermission { - return { type: PermissionType.NFT, ...args }; + +export class NFTCollectionPermission extends Serializable { + collectionAddress: AccountAddress; + + capabilities: Record; + + constructor({ + collectionAddress, + capabilities, + }: { + collectionAddress: AccountAddress; + capabilities: Record; + }) { + super(); + this.collectionAddress = collectionAddress; + this.capabilities = capabilities; + } + + static from = (args: { + collectionAddress: AccountAddress; + capabilities: Record; + }): NFTCollectionPermission => new NFTCollectionPermission(args); + + serialize(serializer: Serializer): void { + this.collectionAddress.serialize(serializer); + + const [capabilityKeys, capabilityValues] = Object.entries(this.capabilities).reduce( + ([keys, values], [key, value]) => [keys.concat(key), values.concat(value)], + [[] as string[], [] as boolean[]], + ); + serializer.serializeStr(JSON.stringify(capabilityKeys)); + serializer.serializeStr(JSON.stringify(capabilityValues)); + } + + static deserialize(deserializer: Deserializer): NFTCollectionPermission { + const collectionAddress = AccountAddress.deserialize(deserializer); + const capabilityKeys = JSON.parse(deserializer.deserializeStr()) as string[]; + const capabilityValues = JSON.parse(deserializer.deserializeStr()) as boolean[]; + const capabilities = capabilityKeys.reduce( + (acc, key, i) => ({ ...acc, [key]: capabilityValues[i] }), + {} as Record, + ); + return NFTCollectionPermission.from({ collectionAddress, capabilities }); + } } diff --git a/tests/e2e/transaction/delegationPermissions.test.ts b/tests/e2e/transaction/delegationPermissions.test.ts index f772fa4e3..55fe86bcb 100644 --- a/tests/e2e/transaction/delegationPermissions.test.ts +++ b/tests/e2e/transaction/delegationPermissions.test.ts @@ -1,10 +1,6 @@ // Copyright © Aptos Foundation // SPDX-License-Identifier: Apache-2.0 -/* eslint-disable no-lone-blocks */ -/* eslint-disable no-console */ -/* eslint-disable no-await-in-loop */ - import { Account, SigningSchemeInput, @@ -19,14 +15,7 @@ import { longTestTimeout } from "../../unit/helper"; import { getAptosClient } from "../helper"; import { fundAccounts, publishTransferPackage } from "./helper"; import { AbstractedEd25519Account } from "../../../src/account/AbstractedAccount"; -import { - buildFungibleAssetPermission, - buildNFTPermission, - buildRevokeFungibleAssetPermission, - Permission, - PermissionType, - RevokePermission, -} from "../../../src/types/permissions"; +import { FungibleAssetPermission, NFTPermission, Permission } from "../../../src/types/permissions"; const LOCAL_NET = getAptosClient(); const CUSTOM_NET = getAptosClient({ @@ -59,8 +48,8 @@ describe("transaction submission", () => { test("Able to re-grant permissions for the same subaccount", async () => { // account - const APT_PERMISSION = buildFungibleAssetPermission({ - asset: AccountAddress.A.toString(), // apt address + const APT_PERMISSION = FungibleAssetPermission.from({ + asset: AccountAddress.A, // apt address balance: "10", }); await requestPermission({ @@ -72,13 +61,13 @@ describe("transaction submission", () => { const perm1 = await aptos.getPermissions({ primaryAccountAddress: primaryAccount.accountAddress, subAccountPublicKey: subAccount.publicKey, - filter: PermissionType.FungibleAsset, + filter: FungibleAssetPermission, }); expect(perm1.length).toBe(1); expect(perm1[0].balance).toBe("10"); - const APT_PERMISSION2 = buildFungibleAssetPermission({ - asset: AccountAddress.A.toString(), + const APT_PERMISSION2 = FungibleAssetPermission.from({ + asset: AccountAddress.A, balance: "20", }); await requestPermission({ @@ -90,7 +79,7 @@ describe("transaction submission", () => { const perm2 = await aptos.getPermissions({ primaryAccountAddress: primaryAccount.accountAddress, subAccountPublicKey: subAccount.publicKey, - filter: PermissionType.FungibleAsset, + filter: FungibleAssetPermission, }); expect(perm2.length).toBe(1); expect(perm2[0].balance).toBe("30"); @@ -104,13 +93,13 @@ describe("transaction submission", () => { await requestPermission({ primaryAccount, permissionedAccount: subAccount, - permissions: [buildNFTPermission({ assetAddress: nftAddress, capabilities: { transfer: true, mutate: false } })], + permissions: [NFTPermission.from({ assetAddress: nftAddress, capabilities: { transfer: true, mutate: false } })], }); const perm1 = await aptos.getPermissions({ primaryAccountAddress: primaryAccount.accountAddress, subAccountPublicKey: subAccount.publicKey, - filter: PermissionType.NFT, + filter: NFTPermission, }); expect(perm1.length).toBe(1); @@ -137,8 +126,8 @@ describe("transaction submission", () => { }, }); - const APT_PERMISSION = buildFungibleAssetPermission({ - asset: AccountAddress.A.toString(), + const APT_PERMISSION = FungibleAssetPermission.from({ + asset: AccountAddress.A, balance: "10", }); await requestPermission({ @@ -150,7 +139,7 @@ describe("transaction submission", () => { const perm1 = await aptos.getPermissions({ primaryAccountAddress: primaryAccount.accountAddress, subAccountPublicKey: subAccount.publicKey, - filter: PermissionType.FungibleAsset, + filter: FungibleAssetPermission, }); expect(perm1.length).toBe(1); expect(perm1[0].balance).toBe("10"); @@ -170,7 +159,7 @@ describe("transaction submission", () => { const perm2 = await aptos.getPermissions({ primaryAccountAddress: primaryAccount.accountAddress, subAccountPublicKey: subAccount.publicKey, - filter: PermissionType.FungibleAsset, + filter: FungibleAssetPermission, }); expect(perm2.length).toBe(1); expect(perm2[0].balance).toBe("9"); @@ -202,13 +191,13 @@ describe("transaction submission", () => { await requestPermission({ primaryAccount, permissionedAccount: subAccount, - permissions: [buildFungibleAssetPermission({ asset: AccountAddress.A.toString(), balance: "10" })], + permissions: [FungibleAssetPermission.from({ asset: AccountAddress.A, balance: "10" })], }); await revokePermission({ primaryAccount, subAccount, - permissions: [buildRevokeFungibleAssetPermission({ asset: AccountAddress.A.toString() })], + permissions: [FungibleAssetPermission.from({ asset: AccountAddress.A, balance: "0" })], }); const txn1 = await signSubmitAndWait({ @@ -237,7 +226,7 @@ describe("transaction submission", () => { await requestPermission({ primaryAccount, permissionedAccount: subAccount, - permissions: [buildFungibleAssetPermission({ asset: AccountAddress.A.toString(), balance: "10" })], + permissions: [FungibleAssetPermission.from({ asset: AccountAddress.A, balance: "10" })], }); const txn1 = await signSubmitAndWait({ @@ -303,7 +292,7 @@ export async function revokePermission({ }: { primaryAccount: SingleKeyAccount; subAccount: AbstractedEd25519Account; - permissions: RevokePermission[]; + permissions: Permission[]; }) { const transaction = await aptos.permissions.revokePermission({ primaryAccountAddress: primaryAccount.accountAddress,