From a2e3972cd923ca8d64be8e04d8891ea8bc6113ac Mon Sep 17 00:00:00 2001 From: Jakub Dzikowski Date: Mon, 3 Feb 2025 20:26:07 +0100 Subject: [PATCH] Align recently added features with SDK v2 conventions (#481) --- .gitignore | 3 +- .vscode/settings.json | 2 +- chain-api/src/types/ChainObject.ts | 2 +- chain-api/src/types/UserProfile.ts | 7 +- chain-api/src/types/allowance.ts | 4 +- chain-api/src/types/bridge.ts | 4 +- chain-api/src/types/burn.ts | 2 +- chain-api/src/types/dtos.ts | 10 +++ chain-api/src/types/fee.ts | 4 +- chain-api/src/types/mint.ts | 20 +++++- chain-api/src/types/oracle.spec.ts | 8 +-- chain-api/src/types/oracle.ts | 16 ++--- chain-api/src/types/swap.ts | 1 + .../src/token/GalaChainTokenContract.ts | 11 ++-- chain-client/src/api/ChainUserAPI.ts | 4 +- chain-test/src/data/users.ts | 2 +- chain-test/src/unit/fixture.ts | 2 +- .../src/__test__/GalaChainTokenContract.ts | 21 +++--- .../src/allowances/fullAllowanceCheck.ts | 12 +++- chaincode/src/burns/burnTokens.ts | 2 +- chaincode/src/contracts/GalaTransaction.ts | 64 +++++++++++-------- .../PublicKeyContract.migration.spec.ts | 3 +- .../src/contracts/PublicKeyContract.spec.ts | 4 +- chaincode/src/contracts/PublicKeyContract.ts | 1 - chaincode/src/contracts/authenticate.ts | 5 +- chaincode/src/contracts/authorize.spec.ts | 13 ++-- chaincode/src/contracts/getCaIdentityAlias.ts | 57 +++++++++++++++++ chaincode/src/contracts/index.ts | 1 + chaincode/src/fees/feeGateImplementations.ts | 51 ++++++++++----- chaincode/src/index.ts | 1 + chaincode/src/mint/constructVerifiedMints.ts | 5 +- chaincode/src/mint/fulfillMint.spec.ts | 12 ++-- chaincode/src/mint/fulfillMint.ts | 56 +++++++++++++--- chaincode/src/mint/index.ts | 3 +- chaincode/src/mint/requestMint.ts | 44 ++++++------- chaincode/src/mint/requestMintAllowance.ts | 2 + chaincode/src/mint/validateMintRequest.ts | 7 +- chaincode/src/services/PublicKeyService.ts | 10 ++- chaincode/src/swaps/index.ts | 4 +- chaincode/src/swaps/requestTokenSwap.ts | 5 +- 40 files changed, 325 insertions(+), 160 deletions(-) create mode 100644 chaincode/src/contracts/getCaIdentityAlias.ts diff --git a/.gitignore b/.gitignore index 71eb2fc919..f7a8427fad 100644 --- a/.gitignore +++ b/.gitignore @@ -29,11 +29,10 @@ node_modules # IDE - VSCode .vscode/* -!.vscode/settings.json +.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json - # misc /.sass-cache /connect.lock diff --git a/.vscode/settings.json b/.vscode/settings.json index 37441beed9..35c483d8ab 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { - "files.eol": "\n" + "files.eol": "\n" } \ No newline at end of file diff --git a/chain-api/src/types/ChainObject.ts b/chain-api/src/types/ChainObject.ts index f9f8f85557..d95277c194 100644 --- a/chain-api/src/types/ChainObject.ts +++ b/chain-api/src/types/ChainObject.ts @@ -69,7 +69,7 @@ export abstract class ChainObject { return instanceToPlain(this); } - public copy(): typeof this { + public copy(): this { // @ts-expect-error type conversion return ChainObject.deserialize(this.constructor, this.toPlainObject()); } diff --git a/chain-api/src/types/UserProfile.ts b/chain-api/src/types/UserProfile.ts index f2136d33fb..ecf1f91fbc 100644 --- a/chain-api/src/types/UserProfile.ts +++ b/chain-api/src/types/UserProfile.ts @@ -12,7 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { IsNotEmpty, IsString, ValidateIf } from "class-validator"; +import { IsNotEmpty, IsOptional, IsString, ValidateIf } from "class-validator"; import { JSONSchema } from "class-validator-jsonschema"; import { IsUserAlias } from "../validators"; @@ -56,8 +56,11 @@ export class UserProfile extends ChainObject { .sort() .join(", ")}, but you can use arbitrary strings to define your own roles.` }) + @IsOptional() @IsString({ each: true }) - roles: string[]; + roles?: string[]; } export const UP_INDEX_KEY = "GCUP"; + +export type UserProfileWithRoles = UserProfile & { roles: string[] }; diff --git a/chain-api/src/types/allowance.ts b/chain-api/src/types/allowance.ts index b5d6f31b41..604a1a3379 100644 --- a/chain-api/src/types/allowance.ts +++ b/chain-api/src/types/allowance.ts @@ -318,7 +318,7 @@ export class GrantAllowanceDto extends SubmitCallDTO { "DTO properties backwards-compatible with prior GrantAllowanceDto, with the " + "exception that this implementation only supports AllowanceType.Mint." }) -export class HighThroughputGrantAllowanceDto extends ChainCallDTO { +export class HighThroughputGrantAllowanceDto extends SubmitCallDTO { // todo: remove all these duplicated properties // it seems something about our @GalaTransaction decorator does not pass through // parent properties. Leaving this class empty with just the `extends GrantAllowanceDto` @@ -371,7 +371,7 @@ export class HighThroughputGrantAllowanceDto extends ChainCallDTO { description: "Experimental: After submitting request to RequestMintAllowance, follow up with FulfillMintAllowance." }) -export class FulfillMintAllowanceDto extends ChainCallDTO { +export class FulfillMintAllowanceDto extends SubmitCallDTO { static MAX_ARR_SIZE = 1000; @ValidateNested({ each: true }) diff --git a/chain-api/src/types/bridge.ts b/chain-api/src/types/bridge.ts index 582a07a9ee..f559f4d6ad 100644 --- a/chain-api/src/types/bridge.ts +++ b/chain-api/src/types/bridge.ts @@ -20,13 +20,13 @@ import { JSONSchema } from "class-validator-jsonschema"; import { BigNumberIsPositive, BigNumberProperty, EnumProperty } from "../validators"; import { ChainId } from "./ChainId"; import { TokenInstance, TokenInstanceKey } from "./TokenInstance"; -import { ChainCallDTO } from "./dtos"; +import { SubmitCallDTO } from "./dtos"; import { OracleBridgeFeeAssertionDto } from "./oracle"; @JSONSchema({ description: "Contains properties of bridge request, i.e. a request to bridge token out from GalaChain." }) -export class RequestTokenBridgeOutDto extends ChainCallDTO { +export class RequestTokenBridgeOutDto extends SubmitCallDTO { @JSONSchema({ description: "The type of the bridge." }) diff --git a/chain-api/src/types/burn.ts b/chain-api/src/types/burn.ts index d1ca9a568e..af972fc2a6 100644 --- a/chain-api/src/types/burn.ts +++ b/chain-api/src/types/burn.ts @@ -124,7 +124,7 @@ export class BurnTokensDto extends SubmitCallDTO { "Mints are executed under the identity of the calling user of this function. " + "All operations occur in the same transaction, meaning either all succeed or none are written to chain." }) -export class BurnAndMintDto extends ChainCallDTO { +export class BurnAndMintDto extends SubmitCallDTO { static MAX_ARR_SIZE = 1000; @JSONSchema({ diff --git a/chain-api/src/types/dtos.ts b/chain-api/src/types/dtos.ts index 6429ca56a8..c28a065cc2 100644 --- a/chain-api/src/types/dtos.ts +++ b/chain-api/src/types/dtos.ts @@ -233,6 +233,16 @@ export class ChainCallDTO { } public sign(privateKey: string, useDer = false): void { + if (useDer) { + if (this.signing === SigningScheme.TON) { + throw new ValidationFailedError("TON signing scheme does not support DER signatures"); + } else { + if (this.signerPublicKey === undefined && this.signerAddress === undefined) { + this.signerPublicKey = signatures.getPublicKey(privateKey); + } + } + } + if (this.signing === SigningScheme.TON) { const keyBuffer = Buffer.from(privateKey, "base64"); this.signature = signatures.ton.getSignature(this, keyBuffer, this.prefix).toString("base64"); diff --git a/chain-api/src/types/fee.ts b/chain-api/src/types/fee.ts index 040d00d8b3..473159ccb2 100644 --- a/chain-api/src/types/fee.ts +++ b/chain-api/src/types/fee.ts @@ -827,7 +827,7 @@ export class FetchFeeThresholdUsesWithPaginationResponse extends ChainCallDTO { "Take an action on a set of provided chain keys, acquired from a fetch response. " + "E.g. ResetFeeThresholds, SettleFeePaymentReceipts, etc." }) -export class ChainKeysDto extends ChainCallDTO { +export class ChainKeysDto extends SubmitCallDTO { @JSONSchema({ description: "A list of composite keys to pass to getObjectsByKeys method." }) @@ -924,7 +924,7 @@ export class SettleFeePaymentReceiptsResponse extends ChainCallDTO { @JSONSchema({ description: "Define a FeeExemption for a specific user." }) -export class FeeExemptionDto extends ChainCallDTO { +export class FeeExemptionDto extends SubmitCallDTO { @JSONSchema({ description: "The user / identity that should be exempt from fees." }) diff --git a/chain-api/src/types/mint.ts b/chain-api/src/types/mint.ts index efd7e9e0bf..615fa69345 100644 --- a/chain-api/src/types/mint.ts +++ b/chain-api/src/types/mint.ts @@ -279,7 +279,7 @@ export class FetchTokenSupplyResponse extends ChainCallDTO { "such that ongoing high throughput mints/mint allowances are migrated " + "to a correct running total." }) -export class PatchMintAllowanceRequestDto extends ChainCallDTO { +export class PatchMintAllowanceRequestDto extends SubmitCallDTO { @JSONSchema({ description: "Token collection." }) @@ -321,7 +321,7 @@ export class PatchMintAllowanceRequestDto extends ChainCallDTO { "such that ongoing high throughput mints/mint allowances are migrated " + "to a correct running total." }) -export class PatchMintRequestDto extends ChainCallDTO { +export class PatchMintRequestDto extends SubmitCallDTO { @JSONSchema({ description: "Token collection." }) @@ -357,7 +357,7 @@ export class PatchMintRequestDto extends ChainCallDTO { @JSONSchema({ description: "DTO that describes a TokenMintConfiguration chain object." }) -export class TokenMintConfigurationDto extends ChainCallDTO { +export class TokenMintConfigurationDto extends SubmitCallDTO { @JSONSchema({ description: "Token collection." }) @@ -484,3 +484,17 @@ export class FetchTokenMintConfigurationsResponse extends ChainCallDTO { @IsString() bookmark: string; } + +export class DeleteTokenMintConfigurationDto extends SubmitCallDTO { + @IsNotEmpty() + public collection: string; + + @IsNotEmpty() + public category: string; + + @IsNotEmpty() + public type: string; + + @IsDefined() + public additionalKey: string; +} diff --git a/chain-api/src/types/oracle.spec.ts b/chain-api/src/types/oracle.spec.ts index 6be853c2f9..ee095925d0 100644 --- a/chain-api/src/types/oracle.spec.ts +++ b/chain-api/src/types/oracle.spec.ts @@ -17,7 +17,7 @@ import { plainToInstance } from "class-transformer"; import { ExternalToken } from "./OraclePriceAssertion"; import { TokenInstanceKey } from "./TokenInstance"; -import { createValidDTO } from "./dtos"; +import { createValidSubmitDTO } from "./dtos"; import { OraclePriceAssertionDto, OraclePriceCrossRateAssertionDto } from "./oracle"; describe("oracle.ts", () => { @@ -54,7 +54,7 @@ describe("oracle.ts", () => { } }; - const tonUsdPriceAssertion = await createValidDTO(OraclePriceAssertionDto, { + const tonUsdPriceAssertion = await createValidSubmitDTO(OraclePriceAssertionDto, { oracle: mockOracle, identity: mockOracleIdentity, externalBaseToken: tonDetails, @@ -63,7 +63,7 @@ describe("oracle.ts", () => { timestamp: 0 }); - const galaUsdPriceAssertion = await createValidDTO(OraclePriceAssertionDto, { + const galaUsdPriceAssertion = await createValidSubmitDTO(OraclePriceAssertionDto, { oracle: mockOracle, identity: mockOracleIdentity, baseToken: galaTokenInstanceKey, @@ -72,7 +72,7 @@ describe("oracle.ts", () => { timestamp: 0 }); - const mockAssertionDto = await createValidDTO(OraclePriceCrossRateAssertionDto, { + const mockAssertionDto = await createValidSubmitDTO(OraclePriceCrossRateAssertionDto, { oracle: mockOracle, identity: mockOracleIdentity, baseTokenCrossRate: tonUsdPriceAssertion, diff --git a/chain-api/src/types/oracle.ts b/chain-api/src/types/oracle.ts index 9ddc8461a0..cf52f12547 100644 --- a/chain-api/src/types/oracle.ts +++ b/chain-api/src/types/oracle.ts @@ -29,19 +29,19 @@ import { } from "class-validator"; import { JSONSchema } from "class-validator-jsonschema"; -import { NotImplementedError, ValidationFailedError } from "../utils"; +import { ValidationFailedError } from "../utils"; import { BigNumberProperty } from "../validators"; import { OracleDefinition } from "./OracleDefinition"; import { ExternalToken, OraclePriceAssertion } from "./OraclePriceAssertion"; import { OraclePriceCrossRateAssertion } from "./OraclePriceCrossRateAssertion"; import { TokenClassKey } from "./TokenClass"; import { TokenInstanceKey } from "./TokenInstance"; -import { ChainCallDTO } from "./dtos"; +import { ChainCallDTO, SubmitCallDTO } from "./dtos"; @JSONSchema({ description: "Save an Oracle definition on chain" }) -export class OracleDefinitionDto extends ChainCallDTO { +export class OracleDefinitionDto extends SubmitCallDTO { @JSONSchema({ description: "Name of the oracle. Unique chain key." }) @@ -123,7 +123,7 @@ export class FetchOracleDefinitionsResponse extends ChainCallDTO { @JSONSchema({ description: "Price data for exchanging two tokens/currenices signed by an Authoritative Oracle" }) -export class OraclePriceAssertionDto extends ChainCallDTO { +export class OraclePriceAssertionDto extends SubmitCallDTO { @JSONSchema({ description: "Name of the oracle defined on chain." }) @@ -221,7 +221,7 @@ export class FetchOraclePriceAssertionsResponse extends ChainCallDTO { @JSONSchema({ description: "Cross Rate Exchange price assertion. E.g. compare $GALA to $ETH via price in $USD for each." }) -export class OraclePriceCrossRateAssertionDto extends ChainCallDTO { +export class OraclePriceCrossRateAssertionDto extends SubmitCallDTO { @JSONSchema({ description: "Name of the oracle defined on chain." }) @@ -386,14 +386,14 @@ export class FetchOraclePriceCrossRateAssertionsResponse extends ChainCallDTO { bookmark?: string; } -export class DeleteOracleAssertionsDto extends ChainCallDTO { +export class DeleteOracleAssertionsDto extends SubmitCallDTO { public static MAX_LIMIT = 1000; @ArrayNotEmpty() chainKeys: string[]; } -export class DeleteOracleDefinitionDto extends ChainCallDTO { +export class DeleteOracleDefinitionDto extends SubmitCallDTO { @IsNotEmpty() name: string; } @@ -401,7 +401,7 @@ export class DeleteOracleDefinitionDto extends ChainCallDTO { @JSONSchema({ description: "Response with signed bridging fee data." }) -export class OracleBridgeFeeAssertionDto extends ChainCallDTO { +export class OracleBridgeFeeAssertionDto extends SubmitCallDTO { @JSONSchema({ description: "Exchange Rate Price Assertion used to calculate Gas Fee" }) diff --git a/chain-api/src/types/swap.ts b/chain-api/src/types/swap.ts index 3445d0a0bc..65d05aba29 100644 --- a/chain-api/src/types/swap.ts +++ b/chain-api/src/types/swap.ts @@ -154,6 +154,7 @@ export class FillTokenSwapDto extends SubmitCallDTO { }) @IsOptional() @ValidateNested() + @Type(() => ExpectedTokenSwap) public expectedTokenSwap?: ExpectedTokenSwap; @JSONSchema({ diff --git a/chain-cli/chaincode-template/src/token/GalaChainTokenContract.ts b/chain-cli/chaincode-template/src/token/GalaChainTokenContract.ts index 52daa83803..915138092e 100644 --- a/chain-cli/chaincode-template/src/token/GalaChainTokenContract.ts +++ b/chain-cli/chaincode-template/src/token/GalaChainTokenContract.ts @@ -208,13 +208,13 @@ export default class GalaChainTokenContract extends GalaContract { in: FullAllowanceCheckDto, out: FullAllowanceCheckResDto }) - public FullAllowanceCheck( + public async FullAllowanceCheck( ctx: GalaChainContext, dto: FullAllowanceCheckDto ): Promise { return fullAllowanceCheck(ctx, { - owner: dto.owner ?? ctx.callingUser, - grantedTo: dto.grantedTo ?? ctx.callingUser, + owner: dto.owner ? await resolveUserAlias(ctx, dto.owner) : ctx.callingUser, + grantedTo: dto.grantedTo ? await resolveUserAlias(ctx, dto.grantedTo) : ctx.callingUser, allowanceType: dto.allowanceType ?? AllowanceType.Use, collection: dto.collection, category: dto.category, @@ -290,7 +290,10 @@ export default class GalaChainTokenContract extends GalaContract { // no signature verification }) public async FulfillMint(ctx: GalaChainContext, dto: FulfillMintDto): Promise { - return fulfillMintRequest(ctx, dto); + return fulfillMintRequest(ctx, { + ...dto, + callingUser: ctx.callingUser + }); } /** diff --git a/chain-client/src/api/ChainUserAPI.ts b/chain-client/src/api/ChainUserAPI.ts index 009f15e943..7a49702827 100644 --- a/chain-client/src/api/ChainUserAPI.ts +++ b/chain-client/src/api/ChainUserAPI.ts @@ -12,11 +12,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { UserAlias } from "@gala-chain/api"; + import { ChainClient, ChainUser } from "../generic"; export interface ChainUserAPI { privateKey: string; - identityKey: string; + identityKey: UserAlias; publicKey: string; ethAddress: string; } diff --git a/chain-test/src/data/users.ts b/chain-test/src/data/users.ts index 6a636e24a2..e9be9fb877 100644 --- a/chain-test/src/data/users.ts +++ b/chain-test/src/data/users.ts @@ -20,7 +20,7 @@ export interface ChainUserWithRoles { ethAddress: string; publicKey: string; privateKey: string; - roles: string[] | undefined; + roles: string[]; } export function randomUser( diff --git a/chain-test/src/unit/fixture.ts b/chain-test/src/unit/fixture.ts index 641440226b..81a4f998ec 100644 --- a/chain-test/src/unit/fixture.ts +++ b/chain-test/src/unit/fixture.ts @@ -165,7 +165,7 @@ class Fixture> { this.ctx.callingUserData = { alias: user.identityKey, ethAddress: user.ethAddress, - roles: user.roles ?? [] + roles: user.roles }; return this; } diff --git a/chaincode/src/__test__/GalaChainTokenContract.ts b/chaincode/src/__test__/GalaChainTokenContract.ts index f1a5e8885a..0a4f58f91e 100644 --- a/chaincode/src/__test__/GalaChainTokenContract.ts +++ b/chaincode/src/__test__/GalaChainTokenContract.ts @@ -63,7 +63,6 @@ import { FulfillTokenSaleDto, FullAllowanceCheckDto, FullAllowanceCheckResDto, - GalaChainResponse, GrantAllowanceDto, HighThroughputMintTokenDto, Loan, @@ -108,7 +107,6 @@ import { GalaTransaction, Submit, UnsignedEvaluate, - batchFillTokenSwapFeeGate, batchMintToken, burnTokens, createTokenClass, @@ -131,8 +129,6 @@ import { fulfillMintRequest, fulfillTokenSale, fullAllowanceCheck, - galaSwapFillFeeGate, - galaSwapRequestFeeGate, grantAllowance, lockToken, lockTokens, @@ -144,7 +140,6 @@ import { removeTokenSale, requestMint, resolveUserAlias, - terminateTokenSwapFeeGate, transferToken, unlockToken, unlockTokens, @@ -154,7 +149,7 @@ import { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore import { version } from "../../package.json"; -import { EVALUATE, Evaluate, SUBMIT } from "../contracts"; +import { SUBMIT, getCaIdentityAlias } from "../contracts"; import { acceptLoanOffer, closeLoan, fetchLoanOffers, fetchLoans, offerLoan } from "../loans"; import { batchFillTokenSwaps, @@ -263,13 +258,13 @@ export default class GalaChainTokenContract extends GalaContract { in: FullAllowanceCheckDto, out: FullAllowanceCheckResDto }) - public FullAllowanceCheck( + public async FullAllowanceCheck( ctx: GalaChainContext, dto: FullAllowanceCheckDto ): Promise { return fullAllowanceCheck(ctx, { - owner: dto.owner ?? ctx.callingUser, - grantedTo: dto.grantedTo ?? ctx.callingUser, + owner: dto.owner ? await resolveUserAlias(ctx, dto.owner) : ctx.callingUser, + grantedTo: dto.grantedTo ? await resolveUserAlias(ctx, dto.grantedTo) : ctx.callingUser, allowanceType: dto.allowanceType ?? AllowanceType.Use, collection: dto.collection, category: dto.category, @@ -342,7 +337,7 @@ export default class GalaChainTokenContract extends GalaContract { // no signature verification }) public async FulfillMint(ctx: GalaChainContext, dto: FulfillMintDto): Promise { - return fulfillMintRequest(ctx, dto); + return fulfillMintRequest(ctx, { requests: dto.requests, callingUser: getCaIdentityAlias(ctx) }); } /** @@ -775,10 +770,10 @@ export default class GalaChainTokenContract extends GalaContract { in: RequestTokenSwapDto, out: TokenSwapRequest }) - public RequestTokenSwap(ctx: GalaChainContext, dto: RequestTokenSwapDto): Promise { + public async RequestTokenSwap(ctx: GalaChainContext, dto: RequestTokenSwapDto): Promise { return requestTokenSwap(ctx, { - offeredBy: dto.offeredBy ?? ctx.callingUser, - offeredTo: dto.offeredTo, + offeredBy: dto.offeredBy ? await resolveUserAlias(ctx, dto.offeredBy) : ctx.callingUser, + offeredTo: dto.offeredTo ? await resolveUserAlias(ctx, dto.offeredTo) : undefined, offered: dto.offered, wanted: dto.wanted, uses: dto.uses, diff --git a/chaincode/src/allowances/fullAllowanceCheck.ts b/chaincode/src/allowances/fullAllowanceCheck.ts index acbb940fdd..f17fa4e151 100644 --- a/chaincode/src/allowances/fullAllowanceCheck.ts +++ b/chaincode/src/allowances/fullAllowanceCheck.ts @@ -12,15 +12,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { AllowanceType, FullAllowanceCheckResDto, TokenAllowance, TokenInstanceKey } from "@gala-chain/api"; +import { + AllowanceType, + FullAllowanceCheckResDto, + TokenAllowance, + TokenInstanceKey, + UserAlias +} from "@gala-chain/api"; import { TokenBalance } from "@gala-chain/api"; import { GalaChainContext } from "../types"; import { getObjectsByPartialCompositeKey, takeUntilUndefined } from "../utils"; export interface FullAllowanceCheckParams { - owner: string; - grantedTo: string; + owner: UserAlias; + grantedTo: UserAlias; allowanceType: AllowanceType; collection?: string; category?: string; diff --git a/chaincode/src/burns/burnTokens.ts b/chaincode/src/burns/burnTokens.ts index 42b3e8fa9a..9386c46e2d 100644 --- a/chaincode/src/burns/burnTokens.ts +++ b/chaincode/src/burns/burnTokens.ts @@ -100,7 +100,7 @@ export async function burnTokens( let applicableAllowanceResponse: TokenAllowance[] = []; // if user is not the owner, check allowances: - if (ctx.callingUser !== owner && !preValidated) { + if (!preValidated && ctx.callingUser !== owner) { // Get allowances const fetchAllowancesData = { grantedTo: ctx.callingUser, diff --git a/chaincode/src/contracts/GalaTransaction.ts b/chaincode/src/contracts/GalaTransaction.ts index 6e57472aef..32d3b2e0d5 100644 --- a/chaincode/src/contracts/GalaTransaction.ts +++ b/chaincode/src/contracts/GalaTransaction.ts @@ -23,6 +23,7 @@ import { Primitive, RuntimeError, SubmitCallDTO, + UserProfile, UserRole, generateResponseSchema, generateSchema, @@ -61,63 +62,71 @@ type GalaTransactionDecoratorFunction = ( descriptor: TypedPropertyDescriptor ) => void; -type OutType = ClassConstructor | Primitive; -type OutArrType = { arrayOf: OutType }; +type OutType = ClassConstructor | Primitive; +type OutArrType = { arrayOf: OutType }; -export type GalaTransactionBeforeFn = (ctx: GalaChainContext, dto: ChainCallDTO) => Promise; +export type GalaTransactionBeforeFn = ( + ctx: GalaChainContext, + dto: In +) => Promise; -export type GalaTransactionAfterFn = ( +export type GalaTransactionAfterFn = ( ctx: GalaChainContext, - dto: ChainCallDTO, - result: GalaChainResponse + dto: In, + result: GalaChainResponse ) => Promise; -export interface CommonTransactionOptions { +export interface CommonTransactionOptions { deprecated?: true; description?: string; - in?: ClassConstructor>; - out?: OutType | OutArrType; + in?: ClassConstructor>; + out?: OutType | OutArrType; /** @deprecated */ allowedOrgs?: string[]; allowedRoles?: string[]; apiMethodName?: string; sequence?: MethodAPI[]; - before?: GalaTransactionBeforeFn; - after?: GalaTransactionAfterFn; + before?: GalaTransactionBeforeFn; + after?: GalaTransactionAfterFn; } -export interface GalaTransactionOptions extends CommonTransactionOptions { +export interface GalaTransactionOptions + extends CommonTransactionOptions { type: GalaTransactionType; verifySignature?: true; enforceUniqueKey?: true; } -export type GalaSubmitOptions = CommonTransactionOptions; +export type GalaSubmitOptions = CommonTransactionOptions; -export interface GalaEvaluateOptions extends CommonTransactionOptions { +export interface GalaEvaluateOptions extends CommonTransactionOptions { verifySignature?: true; } -function isArrayOut(x: OutType | OutArrType | undefined): x is OutArrType { +function isArrayOut(x: OutType | OutArrType | undefined): x is OutArrType { return typeof x === "object" && "arrayOf" in x; } -function Submit(options: GalaSubmitOptions): GalaTransactionDecoratorFunction { +function Submit( + options: GalaSubmitOptions +): GalaTransactionDecoratorFunction { return GalaTransaction({ ...options, type: SUBMIT, verifySignature: true, enforceUniqueKey: true }); } -function Evaluate(options: GalaEvaluateOptions): GalaTransactionDecoratorFunction { +function Evaluate( + options: GalaEvaluateOptions +): GalaTransactionDecoratorFunction { return GalaTransaction({ ...options, type: EVALUATE, verifySignature: true }); } -function UnsignedEvaluate( - options: GalaEvaluateOptions +function UnsignedEvaluate( + options: CommonTransactionOptions ): GalaTransactionDecoratorFunction { return GalaTransaction({ ...options, type: EVALUATE }); } -function GalaTransaction( - options: GalaTransactionOptions +function GalaTransaction( + options: GalaTransactionOptions ): GalaTransactionDecoratorFunction { return (target, propertyKey, descriptor): void => { // Register the DTO class to be passed @@ -169,10 +178,10 @@ function GalaTransaction( ctx?.logger?.logTimeline("Begin Transaction", loggingContext, metadata); // Parse & validate - may throw an exception - const dtoClass = options.in ?? (ChainCallDTO as unknown as ClassConstructor>); + const dtoClass = options.in ?? (ChainCallDTO as unknown as ClassConstructor>); const dto = !dtoPlain ? undefined - : await parseValidDTO(dtoClass, dtoPlain as string | Record); + : await parseValidDTO(dtoClass, dtoPlain as string | Record); // Authenticate the user if (ctx.isDryRun) { @@ -180,8 +189,11 @@ function GalaTransaction( } else if (options?.verifySignature || dto?.signature !== undefined) { ctx.callingUserData = await authenticate(ctx, dto); } else { - // it means a request where authorization is not required. Intentionally misses alias field - ctx.callingUserData = { roles: [UserRole.EVALUATE] }; + // it means a request where authorization is not required. If there is org-based authorization, + // default roles are applied. If not, then only evaluate is possible. Alias is intentionally + // missing. + const roles = !options.allowedOrgs?.length ? [UserRole.EVALUATE] : [...UserProfile.DEFAULT_ROLES]; + ctx.callingUserData = { roles }; } // Authorize the user @@ -196,7 +208,7 @@ function GalaTransaction( } } - const argArray: [GalaChainContext, T] | [GalaChainContext] = dto ? [ctx, dto] : [ctx]; + const argArray: [GalaChainContext, In] | [GalaChainContext] = dto ? [ctx, dto] : [ctx]; if (options?.before !== undefined) { await options?.before?.apply(this, argArray); diff --git a/chaincode/src/contracts/PublicKeyContract.migration.spec.ts b/chaincode/src/contracts/PublicKeyContract.migration.spec.ts index 957606ed7d..a4db2ef9ac 100644 --- a/chaincode/src/contracts/PublicKeyContract.migration.spec.ts +++ b/chaincode/src/contracts/PublicKeyContract.migration.spec.ts @@ -20,6 +20,7 @@ import { SubmitCallDTO, UpdateUserRolesDto, UserProfile, + UserProfileWithRoles, UserRole, createValidSubmitDTO } from "@gala-chain/api"; @@ -60,7 +61,7 @@ describe("Migration from allowedOrgs to allowedRoles", () => { async function getUserProfile() { const dto = new ChainCallDTO().signed(user.privateKey); - const resp = await chaincode.invoke>( + const resp = await chaincode.invoke>( "PublicKeyContract:GetMyProfile", dto ); diff --git a/chaincode/src/contracts/PublicKeyContract.spec.ts b/chaincode/src/contracts/PublicKeyContract.spec.ts index 664cf851d1..2e0d76bfad 100644 --- a/chaincode/src/contracts/PublicKeyContract.spec.ts +++ b/chaincode/src/contracts/PublicKeyContract.spec.ts @@ -551,12 +551,12 @@ describe("GetMyProfile", () => { const dto1 = new GetMyProfileDto(); dto1.signing = SigningScheme.TON; dto1.signerPublicKey = user.publicKey; - dto1.sign(user.privateKey, true); + dto1.sign(user.privateKey); const dto2 = new GetMyProfileDto(); dto2.signing = SigningScheme.TON; dto2.signerAddress = user.tonAddress; - dto2.sign(user.privateKey, true); + dto2.sign(user.privateKey); // When const resp1 = await chaincode.invoke("PublicKeyContract:GetMyProfile", dto1); diff --git a/chaincode/src/contracts/PublicKeyContract.ts b/chaincode/src/contracts/PublicKeyContract.ts index 7ed3e50da1..706c007337 100644 --- a/chaincode/src/contracts/PublicKeyContract.ts +++ b/chaincode/src/contracts/PublicKeyContract.ts @@ -14,7 +14,6 @@ */ import { ChainCallDTO, - GalaChainResponse, GetMyProfileDto, GetPublicKeyDto, NotFoundError, diff --git a/chaincode/src/contracts/authenticate.ts b/chaincode/src/contracts/authenticate.ts index d03ff64ec2..0bd790b6a3 100644 --- a/chaincode/src/contracts/authenticate.ts +++ b/chaincode/src/contracts/authenticate.ts @@ -18,6 +18,7 @@ import { PublicKey, SigningScheme, UserProfile, + UserProfileWithRoles, ValidationFailedError, signatures } from "@gala-chain/api"; @@ -133,7 +134,7 @@ async function getUserProfile( ctx: GalaChainContext, publicKey: string, signing: SigningScheme -): Promise { +): Promise { const address = PublicKeyService.getUserAddress(publicKey, signing); const profile = await PublicKeyService.getUserProfile(ctx, address); @@ -147,7 +148,7 @@ async function getUserProfile( async function getUserProfileAndPublicKey( ctx: GalaChainContext, address -): Promise<{ profile: UserProfile; publicKey: PublicKey }> { +): Promise<{ profile: UserProfileWithRoles; publicKey: PublicKey }> { const profile = await PublicKeyService.getUserProfile(ctx, address); if (profile === undefined) { diff --git a/chaincode/src/contracts/authorize.spec.ts b/chaincode/src/contracts/authorize.spec.ts index bec8afccf7..dca8ad492c 100644 --- a/chaincode/src/contracts/authorize.spec.ts +++ b/chaincode/src/contracts/authorize.spec.ts @@ -72,7 +72,7 @@ describe("authorization", () => { // Then expect(await f1.signedCall()).toEqual(transactionSuccess(registeredUser)); expect(await f1.unsignedCall()).toEqual( - transactionSuccess({ alias: anonymousUserId, roles: [UserRole.EVALUATE] }) + transactionSuccess({ alias: anonymousUserId, roles: [UserRole.EVALUATE, UserRole.SUBMIT] }) ); expect(await f2.signedCall()).toEqual(transactionErrorKey("ORGANIZATION_NOT_ALLOWED")); @@ -94,13 +94,8 @@ describe("authorization", () => { // Then expect(await f1.signedCall()).toEqual(transactionSuccess(registeredUser)); - const f1UnsignedCallResponse = await f1.unsignedCall(); - expect(f1UnsignedCallResponse).toEqual(transactionErrorKey("MISSING_ROLE")); - expect(f1UnsignedCallResponse).toEqual( - transactionErrorMessageContains( - // EVALUATE is default role for anonymous user (no signature) - `User ${anonymousUserId} does not have one of required roles: SUBMIT (has: EVALUATE)` - ) + expect(await f1.unsignedCall()).toEqual( + transactionSuccess({ alias: anonymousUserId, roles: [UserRole.EVALUATE, UserRole.SUBMIT] }) ); expect(await f2.signedCall()).toEqual(transactionErrorKey("ORGANIZATION_NOT_ALLOWED")); @@ -229,7 +224,7 @@ describe("authorization", () => { const user = { ...ChainUser.withRandomKeys(customUser.alias), - roles: customUser.roles + roles: customUser.roles ?? [...UserProfile.DEFAULT_ROLES] }; const ContractClass = TestContractClass(type, verifySignature, allowedOrgs, allowedRoles); diff --git a/chaincode/src/contracts/getCaIdentityAlias.ts b/chaincode/src/contracts/getCaIdentityAlias.ts new file mode 100644 index 0000000000..57e2a9db91 --- /dev/null +++ b/chaincode/src/contracts/getCaIdentityAlias.ts @@ -0,0 +1,57 @@ +/* + * Copyright (c) Gala Games Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { UserAlias, asValidUserAlias } from "@gala-chain/api"; +import { Context } from "fabric-contract-api"; + +const ID_SUB_SPLIT_CHAR = "|"; + +/** + * Get the client's account ID from CA user identity. Not recommended for new code. + * @deprecated + */ +export function getCaIdentityAlias(ctx: Context): UserAlias { + const clientAccountID = ctx.clientIdentity.getID(); + + const OUPrefix = "::/OU="; + const OUSuffix = "/"; + const CNPrefix = "/CN="; + const CNSuffix = ":"; + + if ( + !clientAccountID.includes(OUPrefix) || + !clientAccountID.includes(OUSuffix) || + !clientAccountID.includes(CNPrefix) || + !clientAccountID.includes(CNSuffix) + ) { + throw new Error("Invalid client account ID format"); + } + + // eslint-disable-next-line + const clientAccountIDRegex = /^x509::\/OU\=(.+)\/CN=(.+)\:/; + if (!clientAccountIDRegex.test(clientAccountID)) { + throw new Error("Invalid client account ID format"); + } + + const orgStartIndex = clientAccountID.indexOf(OUPrefix) + OUPrefix.length; + const orgEndIndex = clientAccountID.indexOf(OUSuffix, orgStartIndex); + + const nameStartIndex = clientAccountID.indexOf(CNPrefix) + CNPrefix.length; + const nameEndIndex = clientAccountID.indexOf(CNSuffix, nameStartIndex); + + const org = clientAccountID.slice(orgStartIndex, orgEndIndex); + const name = clientAccountID.slice(nameStartIndex, nameEndIndex); + + return asValidUserAlias(`${org}${ID_SUB_SPLIT_CHAR}${name}`); +} diff --git a/chaincode/src/contracts/index.ts b/chaincode/src/contracts/index.ts index 98931bd668..1f882a64f3 100644 --- a/chaincode/src/contracts/index.ts +++ b/chaincode/src/contracts/index.ts @@ -20,3 +20,4 @@ export * from "./GalaContractApi"; export * from "./GalaTransaction"; export { ensureOrganizationIsAllowed } from "./authorize"; export { OrganizationNotAllowedError } from "./authorize"; +export { getCaIdentityAlias } from "./getCaIdentityAlias"; diff --git a/chaincode/src/fees/feeGateImplementations.ts b/chaincode/src/fees/feeGateImplementations.ts index f33e3cd2e2..8b3eb465b8 100644 --- a/chaincode/src/fees/feeGateImplementations.ts +++ b/chaincode/src/fees/feeGateImplementations.ts @@ -14,6 +14,7 @@ */ import { BatchFillTokenSwapDto, + BatchMintTokenDto, BurnTokensDto, ChainCallDTO, ChainError, @@ -28,6 +29,9 @@ import { FulfillMintAllowanceDto, FulfillMintDto, HighThroughputGrantAllowanceDto, + HighThroughputMintTokenDto, + MintTokenDto, + MintTokenWithAllowanceDto, OracleBridgeFeeAssertion, OracleBridgeFeeAssertionDto, OracleDefinition, @@ -37,6 +41,7 @@ import { RequestTokenBridgeOutDto, TerminateTokenSwapDto, TokenClassKey, + TokenInstanceKey, TokenMintConfiguration, TransferTokenDto, UnauthorizedError, @@ -48,8 +53,9 @@ import BigNumber from "bignumber.js"; import { plainToInstance } from "class-transformer"; import { authenticate } from "../contracts"; -import { MintTokenParams, MintTokenWithAllowanceParams, fetchTokenMintConfiguration } from "../mint"; +import { fetchTokenMintConfiguration } from "../mint"; import { KnownOracles } from "../oracle"; +import { resolveUserAlias } from "../services/resolveUserAlias"; import { GalaChainContext } from "../types"; import { getObjectByKey, putChainObject } from "../utils"; import { burnToMintProcessing } from "./extendedFeeGateProcessing"; @@ -87,9 +93,9 @@ export async function batchFillTokenSwapFeeGate(ctx: GalaChainContext, dto: Batc return galaFeeGate(ctx, { feeCode: FeeGateCodes.BatchFillTokenSwap }); } -export async function batchMintTokenFeeGate(ctx: GalaChainContext, paramsArr: MintTokenParams[]) { +export async function batchMintTokenFeeGate(ctx: GalaChainContext, dto: BatchMintTokenDto) { const feeCode = FeeGateCodes.BatchMintToken; - const owners: string[] = extractUniqueOwnersFromRequests(ctx, paramsArr); + const owners: string[] = extractUniqueOwnersFromRequests(ctx, dto.mintDtos); for (const owner of owners) { await galaFeeGate(ctx, { @@ -103,9 +109,9 @@ export async function batchMintTokenFeeGate(ctx: GalaChainContext, paramsArr: Mi const batchPayments: PaymentRequiredError[] = []; - for (const mintDto of paramsArr) { - const owner = mintDto.owner ?? ctx.callingUser; - const tokenClass = mintDto.tokenClassKey; + for (const mintDto of dto.mintDtos) { + const owner = mintDto.owner ? await resolveUserAlias(ctx, mintDto.owner) : ctx.callingUser; + const tokenClass = mintDto.tokenClass; const quantity = mintDto.quantity; await combinedMintFees(ctx, { feeCode, tokenClass, owner, quantity }).catch((e) => { @@ -139,11 +145,14 @@ export async function terminateTokenSwapFeeGate(ctx: GalaChainContext, dto: Term return galaFeeGate(ctx, { feeCode: FeeGateCodes.TerminateTokenSwap }); } -export async function highThroughputMintRequestFeeGate(ctx: GalaChainContext, dto: MintTokenParams) { - const owner = dto.owner ?? ctx.callingUser; +export async function highThroughputMintRequestFeeGate( + ctx: GalaChainContext, + dto: HighThroughputMintTokenDto +) { + const owner = dto.owner ? await resolveUserAlias(ctx, dto.owner) : ctx.callingUser; const feeCode = FeeGateCodes.HighThroughputMintRequest; - await combinedMintFees(ctx, { feeCode, tokenClass: dto.tokenClassKey, owner, quantity: dto.quantity }); + await combinedMintFees(ctx, { feeCode, tokenClass: dto.tokenClass, owner, quantity: dto.quantity }); } export async function highThroughputMintFulfillFeeGate(ctx: GalaChainContext, dto: FulfillMintDto) { @@ -191,29 +200,39 @@ export async function highThroughputMintAllowanceFulfillFeeGate( return Promise.resolve(); } -export async function mintTokenFeeGate(ctx: GalaChainContext, dto: MintTokenParams) { +export async function mintTokenFeeGate(ctx: GalaChainContext, dto: MintTokenDto) { const feeCode = FeeGateCodes.MintToken; - const owner = dto.owner ?? ctx.callingUser; + const owner = dto.owner ? await resolveUserAlias(ctx, dto.owner) : ctx.callingUser; - await combinedMintFees(ctx, { feeCode, tokenClass: dto.tokenClassKey, owner, quantity: dto.quantity }); + await combinedMintFees(ctx, { feeCode, tokenClass: dto.tokenClass, owner, quantity: dto.quantity }); } export async function mintTokenWithAllowanceFeeGate( ctx: GalaChainContext, - params: MintTokenWithAllowanceParams + params: MintTokenWithAllowanceDto ) { - const owner = params.owner ?? ctx.callingUser; + const owner = params.owner ? await resolveUserAlias(ctx, params.owner) : ctx.callingUser; const feeCode = FeeGateCodes.MintTokenWithAllowance; await combinedMintFees(ctx, { feeCode, - tokenClass: params.tokenClassKey, + tokenClass: params.tokenClass, owner, quantity: params.quantity }); } -export async function requestTokenBridgeOutFeeGate(ctx: GalaChainContext, dto: RequestTokenBridgeOutDto) { +export interface RequestTokenBridgeOutFeeGateParams { + destinationChainId: number; + tokenInstance: TokenInstanceKey; + quantity: BigNumber; + destinationChainTxFee?: OracleBridgeFeeAssertionDto; +} + +export async function requestTokenBridgeOutFeeGate( + ctx: GalaChainContext, + dto: RequestTokenBridgeOutFeeGateParams +) { const { destinationChainId } = dto; // Dynamic, gas based fees are intended for bridging outside of GalaChain diff --git a/chaincode/src/index.ts b/chaincode/src/index.ts index cf2f8061d8..9415036260 100755 --- a/chaincode/src/index.ts +++ b/chaincode/src/index.ts @@ -18,6 +18,7 @@ export * from "./balances"; export * from "./burns"; export * from "./contracts"; export * from "./fees"; +export * from "./loans"; export * from "./locks"; export * from "./mint"; export * from "./oracle"; diff --git a/chaincode/src/mint/constructVerifiedMints.ts b/chaincode/src/mint/constructVerifiedMints.ts index 09b3f0e086..3a6038e46c 100644 --- a/chaincode/src/mint/constructVerifiedMints.ts +++ b/chaincode/src/mint/constructVerifiedMints.ts @@ -18,6 +18,7 @@ import { TokenClass, TokenInstance, TokenMintFulfillment, + UserAlias, createValidChainObject } from "@gala-chain/api"; import BigNumber from "bignumber.js"; @@ -29,9 +30,9 @@ export async function constructVerifiedMints( ctx: GalaChainContext, dto: TokenMintFulfillment, tokenClass: TokenClass, - instanceCounter: BigNumber + instanceCounter: BigNumber, + callingUser: UserAlias ): Promise<[TokenInstance[], TokenBalance]> { - const callingUser = ctx.callingUser; const owner = dto.owner ?? callingUser; const { collection, category, type, additionalKey } = dto; const tokenClassKey = await TokenClass.buildClassKeyObject({ collection, category, type, additionalKey }); diff --git a/chaincode/src/mint/fulfillMint.spec.ts b/chaincode/src/mint/fulfillMint.spec.ts index f69a26f369..817b4d3b84 100644 --- a/chaincode/src/mint/fulfillMint.spec.ts +++ b/chaincode/src/mint/fulfillMint.spec.ts @@ -31,7 +31,7 @@ import { createValidRangedChainObject, createValidSubmitDTO } from "@gala-chain/api"; -import { currency, fixture, nft, users, writesMap } from "@gala-chain/test"; +import { currency, fixture, nft, transactionErrorMessageContains, users, writesMap } from "@gala-chain/test"; import BigNumber from "bignumber.js"; import { plainToInstance } from "class-transformer"; @@ -442,7 +442,7 @@ describe("FulfillMint", () => { expect(getWrites()).toEqual({}); }); - it("prevents attackers from exploiting the lack of signing requirement", async () => { + it("prevents attackers from tampering with mint request", async () => { // Given const nftInstance = nft.tokenInstance1(); const nftInstanceKey = nft.tokenInstance1Key(); @@ -520,7 +520,7 @@ describe("FulfillMint", () => { owner: users.attacker.identityKey // <- tampered here. code expects users.testUser1Id }) ] - }).signed(users.attacker.privateKey); + }); testFixture .savedState(nftClass, nftInstance, tokenAllowance, tokenClaim) @@ -531,10 +531,8 @@ describe("FulfillMint", () => { // Then expect(response).toEqual( - GalaChainResponse.Error( - new Error( - `client|maliciousUser does not have sufficient allowances 0 to Mint 2 token TEST$Item$Potion$Elixir$0 to client|testUser1` - ) + transactionErrorMessageContains( + `owner mismatch: MintRequestDto owner ${users.attacker.identityKey} does not match TokenMintRequest owner ${users.testUser1.identityKey}` ) ); diff --git a/chaincode/src/mint/fulfillMint.ts b/chaincode/src/mint/fulfillMint.ts index 011687c437..cd40be56f2 100644 --- a/chaincode/src/mint/fulfillMint.ts +++ b/chaincode/src/mint/fulfillMint.ts @@ -17,10 +17,8 @@ import { ChainCallDTO, ChainError, ChainObject, - FulfillMintDto, GalaChainResponse, MintRequestDto, - MintTokenDto, RuntimeError, TokenAllowance, TokenClass, @@ -29,7 +27,8 @@ import { TokenInstanceKey, TokenMintFulfillment, TokenMintRequest, - createValidSubmitDTO + UserAlias, + ValidationFailedError } from "@gala-chain/api"; import BigNumber from "bignumber.js"; import { plainToInstance } from "class-transformer"; @@ -124,11 +123,44 @@ export async function mintRequestsByTimeKeys( return requestEntries; } +export interface FulfillMintRequestParams { + requests: MintRequestDto[]; + callingUser: UserAlias; +} + +function validateMintRequestDtoMatchesOriginal( + mintRequestDto: MintRequestDto, + originalRequest: TokenMintRequest +): void { + const fieldsToVerify = [ + ["owner", mintRequestDto.owner, originalRequest.owner], + ["collection", mintRequestDto.collection, originalRequest.collection], + ["category", mintRequestDto.category, originalRequest.category], + ["type", mintRequestDto.type, originalRequest.type], + ["additionalKey", mintRequestDto.additionalKey, originalRequest.additionalKey], + ["timeKey", mintRequestDto.timeKey, originalRequest.timeKey], + [ + "totalKnownMintsCount", + mintRequestDto.totalKnownMintsCount?.toFixed(), + originalRequest.totalKnownMintsCount?.toFixed() + ] + ]; + + for (const [field, dtoValue, reqValue] of fieldsToVerify) { + if (dtoValue !== reqValue) { + const message = `${field} mismatch: MintRequestDto ${field} ${dtoValue} does not match TokenMintRequest ${field} ${reqValue}`; + throw new ValidationFailedError(message, { + dto: mintRequestDto, + originalRequest + }); + } + } +} + export async function fulfillMintRequest( ctx: GalaChainContext, - dto: FulfillMintDto + { requests, callingUser }: FulfillMintRequestParams ): Promise> { - const requests = dto.requests; const requestIds = requests.map((r) => r.id); const reqIdx: Record = indexMintRequests(requests); @@ -235,6 +267,12 @@ export async function fulfillMintRequest( continue; } + // Add validation that the request matches the original mint request + const mintRequestDto = requests.find((r) => r.id === req.id); + if (mintRequestDto) { + validateMintRequestDtoMatchesOriginal(mintRequestDto, req); + } + let mintableQty = false; let qtyError: ChainError | undefined; @@ -274,7 +312,8 @@ export async function fulfillMintRequest( const applicableAllowances: TokenAllowance[] = await validateMintRequest( ctx, mintReqParams, - tokenClass + tokenClass, + callingUser ); const actionDescription = @@ -303,7 +342,8 @@ export async function fulfillMintRequest( ctx, mintFulfillmentEntry, tokenClass, - instanceCounter + instanceCounter, + callingUser ); const returnKeys: Array = []; @@ -334,7 +374,7 @@ export async function fulfillMintRequest( await Promise.all(successful.map((mintFulfillment) => putChainObject(ctx, mintFulfillment))); - if (resultInstanceKeys.length < dto.requests.length) { + if (resultInstanceKeys.length < requests.length) { throw new Error( JSON.stringify({ success: resultInstanceKeys, diff --git a/chaincode/src/mint/index.ts b/chaincode/src/mint/index.ts index 54604551f9..0e59f660de 100644 --- a/chaincode/src/mint/index.ts +++ b/chaincode/src/mint/index.ts @@ -32,7 +32,7 @@ import { fulfillMintAllowanceRequest } from "./fulfillMintAllowance"; import { indexMintRequests } from "./indexMintRequests"; import { MintTokenParams, UpdateTokenSupplyParams, mintToken } from "./mintToken"; import { MintTokenWithAllowanceParams, mintTokenWithAllowance } from "./mintTokenWithAllowance"; -import { WriteMintRequestParams, requestMint } from "./requestMint"; +import { WriteMintRequestParams, requestMint, requestMintBatch } from "./requestMint"; import { InternalGrantAllowanceData, requestMintAllowance } from "./requestMintAllowance"; import { saveTokenMintConfiguration } from "./saveMintConfiguration"; import { validateMintRequest } from "./validateMintRequest"; @@ -54,6 +54,7 @@ export { requestMint, WriteMintRequestParams, requestMintAllowance, + requestMintBatch, InternalGrantAllowanceData, saveTokenMintConfiguration, validateMintRequest, diff --git a/chaincode/src/mint/requestMint.ts b/chaincode/src/mint/requestMint.ts index e25902a41d..d3b04fbb58 100644 --- a/chaincode/src/mint/requestMint.ts +++ b/chaincode/src/mint/requestMint.ts @@ -15,13 +15,9 @@ import { AllowanceKey, AuthorizedOnBehalf, - BatchMintTokenDto, - BigNumberIsNotNegative, - BigNumberProperty, FulfillMintDto, - HighThroughputMintTokenDto, - IsUserRef, MintRequestDto, + TokenAllowance, TokenClass, TokenClassKey, TokenClassKeyProperties, @@ -31,9 +27,7 @@ import { createValidSubmitDTO } from "@gala-chain/api"; import BigNumber from "bignumber.js"; -import { Type, plainToInstance } from "class-transformer"; -import { IsNotEmpty, IsOptional } from "class-validator"; -import { JSONSchema } from "class-validator-jsonschema"; +import { plainToInstance } from "class-transformer"; import { GalaChainContext } from "../types"; import { getObjectByKey, inverseEpoch, inverseTime, putRangedChainObject } from "../utils"; @@ -70,33 +64,37 @@ export async function requestMint(ctx: GalaChainContext, params: RequestMintPara return resDto; } -export interface MintTokenParams { - tokenClass: TokenClassKey; - owner: UserAlias | undefined; +export interface MintOperationParams { + tokenClassKey: TokenClassKey; + owner: UserAlias; quantity: BigNumber; - allowanceKey: AllowanceKey | undefined; -} - -export interface RequestBatchMintParams { - mints: MintTokenParams[]; authorizedOnBehalf: AuthorizedOnBehalf | undefined; + applicableAllowances?: TokenAllowance[] | undefined; + knownTotalSupply?: BigNumber | undefined; } export async function requestMintBatch( ctx: GalaChainContext, - params: RequestBatchMintParams + ops: MintOperationParams[] ): Promise { const minted: Array = []; const errors: Array = []; // mints sequentially; fails all mints if at least one operation fails - for (let i = 0; i < params.mints.length; i += 1) { - const mintParams = { ...params.mints[i], authorizedOnBehalf: params.authorizedOnBehalf }; + for (let i = 0; i < ops.length; i += 1) { + const mintDto = { + tokenClass: ops[i].tokenClassKey, + quantity: ops[i].quantity, + owner: ops[i].owner, + authorizedOnBehalf: ops[i].authorizedOnBehalf, + allowanceKey: undefined + }; + try { - const mintRequest = await submitMintRequest(ctx, mintParams); + const mintRequest = await submitMintRequest(ctx, mintDto); const mintRequestDto = plainToInstance(MintRequestDto, mintRequest); - mintRequestDto.allowanceKey = mintParams.allowanceKey; + mintRequestDto.id = mintRequest.requestId(); minted.push(mintRequestDto); } catch (e) { @@ -107,7 +105,7 @@ export async function requestMintBatch( if (errors.length > 0) { throw new Error(`No token was minted. Errors: ${errors.join("; ")}.`); } else { - const resDto = await createValidSubmitDTO(FulfillMintDto, { + const resDto = plainToInstance(FulfillMintDto, { requests: minted }); @@ -134,7 +132,7 @@ export async function submitMintRequest( if (!tokenClass) throw new Error("missing tokenclass"); - await validateMintRequest(ctx, params, tokenClass).catch((e) => { + await validateMintRequest(ctx, params, tokenClass, callingUser).catch((e) => { throw new Error(`ValidateMintRequest failure: ${e.message}`); }); diff --git a/chaincode/src/mint/requestMintAllowance.ts b/chaincode/src/mint/requestMintAllowance.ts index 97457b12cc..76a441ec28 100644 --- a/chaincode/src/mint/requestMintAllowance.ts +++ b/chaincode/src/mint/requestMintAllowance.ts @@ -39,6 +39,7 @@ export interface InternalGrantAllowanceData { quantities: Array; uses: BigNumber; expires?: number; + uniqueKey: string; } export async function requestMintAllowance( @@ -125,6 +126,7 @@ export async function requestMintAllowance( const res = new FulfillMintAllowanceDto(); res.requests = successfulRequests; + res.uniqueKey = `${dto.uniqueKey}-fulfill`; return res; } diff --git a/chaincode/src/mint/validateMintRequest.ts b/chaincode/src/mint/validateMintRequest.ts index b397c2c822..12d9e6d87f 100644 --- a/chaincode/src/mint/validateMintRequest.ts +++ b/chaincode/src/mint/validateMintRequest.ts @@ -18,7 +18,6 @@ import { AuthorizedOnBehalf, ChainCallDTO, ChainObject, - HighThroughputMintTokenDto, TokenAllowance, TokenClass, TokenClassKeyProperties, @@ -43,9 +42,9 @@ export interface ValidateMintRequestParams { export async function validateMintRequest( ctx: GalaChainContext, params: ValidateMintRequestParams, - tokenClass: TokenClass + tokenClass: TokenClass, + callingUser: UserAlias ): Promise { - const callingUser: string = ctx.callingUser; const owner = params.owner ?? callingUser; const tokenClassKey = params.tokenClass; const quantity = params.quantity; @@ -124,7 +123,7 @@ export async function validateMintRequest( if (totalAllowance.isLessThan(quantity)) { throw new Error( - `${callingUser} does not have sufficient allowances ${totalAllowance.toString()} to ${actionDescription}` + `${callingOnBehalf} does not have sufficient allowances ${totalAllowance.toString()} to ${actionDescription}` ); } diff --git a/chaincode/src/services/PublicKeyService.ts b/chaincode/src/services/PublicKeyService.ts index 916fa21a30..7ee89c8c24 100644 --- a/chaincode/src/services/PublicKeyService.ts +++ b/chaincode/src/services/PublicKeyService.ts @@ -22,6 +22,7 @@ import { UnauthorizedError, UserAlias, UserProfile, + UserProfileWithRoles, normalizePublicKey, signatures } from "@gala-chain/api"; @@ -97,7 +98,10 @@ export class PublicKeyService { : signatures.getEthAddress(signatures.getNonCompactHexPublicKey(publicKey)); } - public static async getUserProfile(ctx: Context, address: string): Promise { + public static async getUserProfile( + ctx: Context, + address: string + ): Promise { const key = PublicKeyService.getUserProfileKey(ctx, address); const data = await ctx.stub.getState(key); @@ -108,7 +112,7 @@ export class PublicKeyService { userProfile.roles = Array.from(UserProfile.DEFAULT_ROLES); } - return userProfile; + return userProfile as UserProfileWithRoles; } // check if we want the profile of the admin @@ -135,7 +139,7 @@ export class PublicKeyService { adminProfile.alias = alias; adminProfile.roles = Array.from(UserProfile.ADMIN_ROLES); - return adminProfile; + return adminProfile as UserProfileWithRoles; } } diff --git a/chaincode/src/swaps/index.ts b/chaincode/src/swaps/index.ts index 3faa53a57b..fd0167d667 100644 --- a/chaincode/src/swaps/index.ts +++ b/chaincode/src/swaps/index.ts @@ -13,6 +13,7 @@ * limitations under the License. */ import { batchFillTokenSwaps } from "./batchFillTokenSwaps"; +import { cleanTokenSwaps } from "./cleanExpiredSwaps"; import { ensureTokenSwapIndexing } from "./ensureTokenSwapIndexing"; import { fetchTokenSwapByRequestId } from "./fetchTokenSwapByRequestId"; import { fetchTokenSwaps } from "./fetchTokenSwaps"; @@ -36,5 +37,6 @@ export { fetchTokenSwapsOfferedToUser, fillTokenSwap, requestTokenSwap, - terminateTokenSwap + terminateTokenSwap, + cleanTokenSwaps }; diff --git a/chaincode/src/swaps/requestTokenSwap.ts b/chaincode/src/swaps/requestTokenSwap.ts index 42c8a705d1..d775c4eb6a 100644 --- a/chaincode/src/swaps/requestTokenSwap.ts +++ b/chaincode/src/swaps/requestTokenSwap.ts @@ -25,6 +25,7 @@ import { TokenSwapRequestInstanceWanted, TokenSwapRequestOfferedBy, TokenSwapRequestOfferedTo, + UserAlias, asValidUserAlias } from "@gala-chain/api"; import { BigNumber } from "bignumber.js"; @@ -58,8 +59,8 @@ function validateTokenSwapQuantity(quantity: BigNumber, tokenClass: TokenClass): } interface RequestTokenSwapParams { - offeredBy: string; - offeredTo?: string; + offeredBy: UserAlias; + offeredTo?: UserAlias; offered: TokenInstanceQuantity[]; wanted: TokenInstanceQuantity[]; uses: BigNumber;