diff --git a/.prettierrc b/.prettierrc index 07a2cea..ac0fa89 100644 --- a/.prettierrc +++ b/.prettierrc @@ -9,4 +9,3 @@ "arrowParens": "always", "endOfLine": "lf" } - diff --git a/docs/test-specifications/token-service/tokenFreezeTransaction.md b/docs/test-specifications/token-service/TokenFreezeTransaction.md similarity index 100% rename from docs/test-specifications/token-service/tokenFreezeTransaction.md rename to docs/test-specifications/token-service/TokenFreezeTransaction.md diff --git a/docs/test-specifications/token-service/TokenGrantKycTransaction.md b/docs/test-specifications/token-service/TokenGrantKycTransaction.md index 2e62866..013a3ab 100644 --- a/docs/test-specifications/token-service/TokenGrantKycTransaction.md +++ b/docs/test-specifications/token-service/TokenGrantKycTransaction.md @@ -54,19 +54,19 @@ The tests contained in this specification will assume that a valid account and a | Test no | Name | Input | Expected response | Implemented (Y/N) | |---------|------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------|-------------------| -| 1 | Grants KYC of a token to an account | tokenId=, accountId=, commonTransactionParams.signers=[] | The token grants KYC to the account. | N | -| 2 | Grants KYC of a token that doesn't exist to an account | tokenId="123.456.789", accountId= | The token KYC grant fails with an INVALID_TOKEN_ID response code from the network. | N | -| 3 | Grants KYC of a token with an empty token ID to an account | tokenId="", accountId= | The token KYC grant fails with an SDK internal error. | N | -| 4 | Grants KYC of a token with no token ID to an account | accountId= | The token KYC grant fails with an INVALID_TOKEN_ID response code from the network. | N | -| 5 | Grants KYC of a deleted token to an account | tokenId=, accountId=, commonTransactionParams.signers=[] | The token KYC grant fails with an TOKEN_WAS_DELETED response code from the network. | N | -| 6 | Grants KYC of a token to an account without signing with the token's KYC key | tokenId=, accountId= | The token KYC grant fails with an INVALID_SIGNATURE response code from the network. | N | -| 7 | Grants KYC of a token to an account but signs with the the token's admin key | tokenId=, accountId=, commonTransactionParams.signers=[] | The token KYC grant fails with an INVALID_SIGNATURE response code from the network. | N | -| 8 | Grants KYC of a token to an account but signs with an incorrect private key | tokenId=, accountId=, commonTransactionParams.signers=[] | The token KYC grant fails with an INVALID_SIGNATURE response code from the network. | N | -| 9 | Grants KYC of a token with no KYC key to an account | tokenId=, accountId= | The token KYC grant fails with an TOKEN_HAS_NO_KYC_KEY response code from the network. | N | -| 10 | Grants KYC of a token to an account that already has KYC | tokenId=, accountId=, commonTransactionParams.signers=[] | The token grants KYC to the account. | N | -| 11 | Grants KYC of a token to an account that is not associated with the token | tokenId=, accountId=, commonTransactionParams.signers=[] | The token KYC grant fails with an TOKEN_NOT_ASSOCIATED_TO_ACCOUNT response code from the network. | N | -| 12 | Grants KYC of a paused token to an account | tokenId=, accountId=, commonTransactionParams.signers=[] | The token KYC grant fails with an TOKEN_IS_PAUSED response code from the network. | N | -| 13 | Grants KYC of a token to a frozen account | tokenId=, accountId=, commonTransactionParams.signers=[] | The token KYC grant fails with an ACCOUNT_FROZEN_FOR_TOKEN response code from the network. | N | +| 1 | Grants KYC of a token to an account | tokenId=, accountId=, commonTransactionParams.signers=[] | The token grants KYC to the account. | Y | +| 2 | Grants KYC of a token that doesn't exist to an account | tokenId="123.456.789", accountId= | The token KYC grant fails with an INVALID_TOKEN_ID response code from the network. | Y | +| 3 | Grants KYC of a token with an empty token ID to an account | tokenId="", accountId= | The token KYC grant fails with an SDK internal error. | Y | +| 4 | Grants KYC of a token with no token ID to an account | accountId= | The token KYC grant fails with an INVALID_TOKEN_ID response code from the network. | Y | +| 5 | Grants KYC of a deleted token to an account | tokenId=, accountId=, commonTransactionParams.signers=[] | The token KYC grant fails with an TOKEN_WAS_DELETED response code from the network. | Y | +| 6 | Grants KYC of a token to an account without signing with the token's KYC key | tokenId=, accountId= | The token KYC grant fails with an INVALID_SIGNATURE response code from the network. | Y | +| 7 | Grants KYC of a token to an account but signs with the the token's admin key | tokenId=, accountId=, commonTransactionParams.signers=[] | The token KYC grant fails with an INVALID_SIGNATURE response code from the network. | Y | +| 8 | Grants KYC of a token to an account but signs with an incorrect private key | tokenId=, accountId=, commonTransactionParams.signers=[] | The token KYC grant fails with an INVALID_SIGNATURE response code from the network. | Y | +| 9 | Grants KYC of a token with no KYC key to an account | tokenId=, accountId= | The token KYC grant fails with an TOKEN_HAS_NO_KYC_KEY response code from the network. | Y | +| 10 | Grants KYC of a token to an account that already has KYC | tokenId=, accountId=, commonTransactionParams.signers=[] | The token grants KYC to the account. | Y | +| 11 | Grants KYC of a token to an account that is not associated with the token | tokenId=, accountId=, commonTransactionParams.signers=[] | The token KYC grant fails with an TOKEN_NOT_ASSOCIATED_TO_ACCOUNT response code from the network. | Y | +| 12 | Grants KYC of a paused token to an account | tokenId=, accountId=, commonTransactionParams.signers=[] | The token KYC grant fails with an TOKEN_IS_PAUSED response code from the network. | Y | +| 13 | Grants KYC of a token to a frozen account | tokenId=, accountId=, commonTransactionParams.signers=[] | The token KYC grant fails with an ACCOUNT_FROZEN_FOR_TOKEN response code from the network. | Y | #### JSON Request Example @@ -103,12 +103,12 @@ The tests contained in this specification will assume that a valid account and a - The ID of the account to which to grant KYC. -| Test no | Name | Input | Expected response | Implemented (Y/N) | -|---------|--------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------|-------------------| -| 1 | Grants KYC of a token to an account that doesn't exist | tokenId=, accountId="123.456.789", commonTransactionParams.signers=[] | The token KYC grant fails with an INVALID_ACCOUNT_ID response code from the network. | N | -| 2 | Grants KYC of a token to an empty account ID | tokenId=, accountId="", commonTransactionParams.signers=[] | The token KYC grant fails with an SDK internal error. | N | -| 3 | Grants KYC of a token to an account with no account ID | tokenId=, commonTransactionParams.signers=[] | The token KYC grant fails with an INVALID_ACCOUNT_ID response code from the network. | N | -| 4 | Grants KYC of a token to a deleted account | tokenId=, accountId=, commonTransactionParams.signers=[] | The token KYC grant fails with an ACCOUNT_WAS_DELETED response code from the network. | N | +| Test no | Name | Input | Expected response | Implemented (Y/N) | +|---------|--------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------|-------------------| +| 1 | Grants KYC of a token to an account that doesn't exist | tokenId=, accountId="123.456.789", commonTransactionParams.signers=[] | The token KYC grant fails with an INVALID_ACCOUNT_ID response code from the network. | Y | +| 2 | Grants KYC of a token to an empty account ID | tokenId=, accountId="", commonTransactionParams.signers=[] | The token KYC grant fails with an SDK internal error. | Y | +| 3 | Grants KYC of a token to an account with no account ID | tokenId=, commonTransactionParams.signers=[] | The token KYC grant fails with an INVALID_ACCOUNT_ID response code from the network. | Y | +| 4 | Grants KYC of a token to a deleted account | tokenId=, accountId=, commonTransactionParams.signers=[] | The token KYC grant fails with an ACCOUNT_DELETED response code from the network. | Y | #### JSON Request Example diff --git a/docs/test-specifications/token-service/tokenUnfreezeTransaction.md b/docs/test-specifications/token-service/TokenUnfreezeTransaction.md similarity index 100% rename from docs/test-specifications/token-service/tokenUnfreezeTransaction.md rename to docs/test-specifications/token-service/TokenUnfreezeTransaction.md diff --git a/src/services/MirrorNodeClient.ts b/src/services/MirrorNodeClient.ts index 75e459e..29040e1 100644 --- a/src/services/MirrorNodeClient.ts +++ b/src/services/MirrorNodeClient.ts @@ -25,6 +25,12 @@ class MirrorNodeClient { const url = `${this.mirrorNodeRestUrl}/api/v1/tokens/${tokenId}`; return retryOnError(async () => fetchData(url)); } + + // TODO: Get mirror node interface with OpenAPI + async getTokenRelationships(accountId: string): Promise { + const url = `${this.mirrorNodeRestUrl}/api/v1/accounts/${accountId}/tokens`; + return retryOnError(async () => fetchData(url)); + } } export default new MirrorNodeClient(); diff --git a/src/tests/token-service/test-token-grant-kyc-transaction.ts b/src/tests/token-service/test-token-grant-kyc-transaction.ts new file mode 100644 index 0000000..db32386 --- /dev/null +++ b/src/tests/token-service/test-token-grant-kyc-transaction.ts @@ -0,0 +1,442 @@ +import { assert, expect } from "chai"; + +import { JSONRPCRequest } from "@services/Client"; +import mirrorNodeClient from "@services/MirrorNodeClient"; + +import { setOperator } from "@helpers/setup-tests"; +import { retryOnError } from "@helpers/retry-on-error"; + +/** + * Tests for TokenGrantKycTransaction + */ +describe("TokenGrantKycTransaction", function () { + // Tests should not take longer than 30 seconds to fully execute. + this.timeout(30000); + + // All tests require an account and a token to be created and to have the two be associated. + let tokenId: string, + tokenFreezeKey: string, + tokenAdminKey: string, + tokenPauseKey: string, + tokenKycKey: string, + accountId: string, + accountPrivateKey: string; + beforeEach(async function () { + await setOperator( + this, + process.env.OPERATOR_ACCOUNT_ID as string, + process.env.OPERATOR_ACCOUNT_PRIVATE_KEY as string, + ); + + tokenFreezeKey = ( + await JSONRPCRequest(this, "generateKey", { + type: "ed25519PrivateKey", + }) + ).key; + + tokenAdminKey = ( + await JSONRPCRequest(this, "generateKey", { + type: "ed25519PrivateKey", + }) + ).key; + + tokenPauseKey = ( + await JSONRPCRequest(this, "generateKey", { + type: "ecdsaSecp256k1PrivateKey", + }) + ).key; + + tokenKycKey = ( + await JSONRPCRequest(this, "generateKey", { + type: "ecdsaSecp256k1PrivateKey", + }) + ).key; + + tokenId = ( + await JSONRPCRequest(this, "createToken", { + name: "testname", + symbol: "testsymbol", + treasuryAccountId: process.env.OPERATOR_ACCOUNT_ID, + adminKey: tokenAdminKey, + kycKey: tokenKycKey, + freezeKey: tokenFreezeKey, + pauseKey: tokenPauseKey, + commonTransactionParams: { + signers: [tokenAdminKey], + }, + }) + ).tokenId; + + accountPrivateKey = ( + await JSONRPCRequest(this, "generateKey", { + type: "ed25519PrivateKey", + }) + ).key; + + accountId = ( + await JSONRPCRequest(this, "createAccount", { + key: accountPrivateKey, + }) + ).accountId; + + await JSONRPCRequest(this, "associateToken", { + accountId, + tokenIds: [tokenId], + commonTransactionParams: { + signers: [accountPrivateKey], + }, + }); + }); + afterEach(async function () { + await JSONRPCRequest(this, "reset"); + }); + + async function verifyTokenKyc(accountId: string, tokenId: string) { + // No way to get token associations via consensus node, so just query mirror node. + const mirrorNodeInfo = + await mirrorNodeClient.getTokenRelationships(accountId); + + let foundToken = false; + for (let i = 0; i < mirrorNodeInfo.tokens.length; i++) { + if (mirrorNodeInfo.tokens[i].token_id === tokenId) { + expect(mirrorNodeInfo.tokens[i].kyc_status).to.equal("GRANTED"); + foundToken = true; + break; + } + } + + if (!foundToken) { + assert.fail("Token ID not found"); + } + } + + describe("Token ID", function () { + it("(#1) Grants KYC of a token to an account", async function () { + await JSONRPCRequest(this, "grantTokenKyc", { + tokenId, + accountId, + commonTransactionParams: { + signers: [tokenKycKey], + }, + }); + + await retryOnError(async function () { + await verifyTokenKyc(accountId, tokenId); + }); + }); + + it("(#2) Grants KYC of a token that doesn't exist to an account", async function () { + try { + await JSONRPCRequest(this, "grantTokenKyc", { + tokenId: "123.456.789", + accountId, + }); + } catch (err: any) { + assert.equal(err.data.status, "INVALID_TOKEN_ID"); + return; + } + + assert.fail("Should throw an error"); + }); + + it("(#3) Grants KYC of a token with an empty token ID to an account", async function () { + try { + await JSONRPCRequest(this, "grantTokenKyc", { + tokenId: "", + accountId, + }); + } catch (err: any) { + assert.equal(err.code, -32603, "Internal error"); + return; + } + + assert.fail("Should throw an error"); + }); + + it("(#4) Grants KYC of a token with no token ID to an account", async function () { + try { + await JSONRPCRequest(this, "grantTokenKyc", { + accountId, + }); + } catch (err: any) { + assert.equal(err.data.status, "INVALID_TOKEN_ID"); + return; + } + + assert.fail("Should throw an error"); + }); + + it("(#5) Grants KYC of a deleted token to an account", async function () { + await JSONRPCRequest(this, "deleteToken", { + tokenId, + commonTransactionParams: { + signers: [tokenAdminKey], + }, + }); + + try { + await JSONRPCRequest(this, "grantTokenKyc", { + tokenId, + accountId, + commonTransactionParams: { + signers: [tokenKycKey], + }, + }); + } catch (err: any) { + assert.equal(err.data.status, "TOKEN_WAS_DELETED"); + return; + } + + assert.fail("Should throw an error"); + }); + + it("(#6) Grants KYC of a token to an account without signing with the token's KYC key", async function () { + try { + await JSONRPCRequest(this, "grantTokenKyc", { + tokenId, + accountId, + }); + } catch (err: any) { + assert.equal(err.data.status, "INVALID_SIGNATURE"); + return; + } + + assert.fail("Should throw an error"); + }); + + it("(#7) Grants KYC of a token to an account but signs with the the token's admin key", async function () { + try { + await JSONRPCRequest(this, "grantTokenKyc", { + tokenId, + accountId, + commonTransactionParams: { + signers: [tokenAdminKey], + }, + }); + } catch (err: any) { + assert.equal(err.data.status, "INVALID_SIGNATURE"); + return; + } + + assert.fail("Should throw an error"); + }); + + it("(#8) Grants KYC of a token to an account but signs with an incorrect private key", async function () { + try { + await JSONRPCRequest(this, "grantTokenKyc", { + tokenId, + accountId, + commonTransactionParams: { + signers: [ + ( + await JSONRPCRequest(this, "generateKey", { + type: "ed25519PrivateKey", + }) + ).key, + ], + }, + }); + } catch (err: any) { + assert.equal(err.data.status, "INVALID_SIGNATURE"); + return; + } + + assert.fail("Should throw an error"); + }); + + it("(#9) Grants KYC of a token with no KYC key to an account", async function () { + try { + await JSONRPCRequest(this, "grantTokenKyc", { + tokenId: ( + await JSONRPCRequest(this, "createToken", { + name: "testname", + symbol: "testsymbol", + treasuryAccountId: process.env.OPERATOR_ACCOUNT_ID, + }) + ).tokenId, + accountId, + }); + } catch (err: any) { + assert.equal(err.data.status, "TOKEN_HAS_NO_KYC_KEY"); + return; + } + + assert.fail("Should throw an error"); + }); + + it("(#10) Grants KYC of a token to an account that already has KYC", async function () { + await JSONRPCRequest(this, "grantTokenKyc", { + tokenId, + accountId, + commonTransactionParams: { + signers: [tokenKycKey], + }, + }); + + await JSONRPCRequest(this, "grantTokenKyc", { + tokenId, + accountId, + commonTransactionParams: { + signers: [tokenKycKey], + }, + }); + + await retryOnError(async function () { + await verifyTokenKyc(accountId, tokenId); + }); + }); + + it("(#11) Grants KYC of a token to an account that is not associated with the token", async function () { + await JSONRPCRequest(this, "dissociateToken", { + accountId, + tokenIds: [tokenId], + commonTransactionParams: { + signers: [accountPrivateKey], + }, + }); + + try { + await JSONRPCRequest(this, "grantTokenKyc", { + tokenId, + accountId, + commonTransactionParams: { + signers: [tokenKycKey], + }, + }); + } catch (err: any) { + assert.equal(err.data.status, "TOKEN_NOT_ASSOCIATED_TO_ACCOUNT"); + return; + } + + assert.fail("Should throw an error"); + }); + + it("(#12) Grants KYC of a paused token to an account", async function () { + await JSONRPCRequest(this, "pauseToken", { + tokenId, + commonTransactionParams: { + signers: [tokenPauseKey], + }, + }); + + try { + await JSONRPCRequest(this, "grantTokenKyc", { + tokenId, + accountId, + commonTransactionParams: { + signers: [tokenKycKey], + }, + }); + } catch (err: any) { + assert.equal(err.data.status, "TOKEN_IS_PAUSED"); + return; + } + + assert.fail("Should throw an error"); + }); + + it("(#13) Grants KYC of a token to a frozen account", async function () { + await JSONRPCRequest(this, "freezeToken", { + tokenId, + accountId, + commonTransactionParams: { + signers: [tokenFreezeKey], + }, + }); + + try { + await JSONRPCRequest(this, "grantTokenKyc", { + tokenId, + accountId, + commonTransactionParams: { + signers: [tokenKycKey], + }, + }); + } catch (err: any) { + assert.equal(err.data.status, "ACCOUNT_FROZEN_FOR_TOKEN"); + return; + } + + assert.fail("Should throw an error"); + }); + }); + + describe("Account ID", function () { + it("(#1) Grants KYC of a token to an account that doesn't exist", async function () { + try { + await JSONRPCRequest(this, "grantTokenKyc", { + tokenId, + accountId: "123.456.789", + commonTransactionParams: { + signers: [tokenKycKey], + }, + }); + } catch (err: any) { + assert.equal(err.data.status, "INVALID_ACCOUNT_ID"); + return; + } + + assert.fail("Should throw an error"); + }); + + it("(#2) Grants KYC of a token to an empty account ID", async function () { + try { + await JSONRPCRequest(this, "grantTokenKyc", { + tokenId, + accountId: "", + commonTransactionParams: { + signers: [tokenKycKey], + }, + }); + } catch (err: any) { + assert.equal(err.code, -32603, "Internal error"); + return; + } + + assert.fail("Should throw an error"); + }); + + it("(#3) Grants KYC of a token to an account with no account ID", async function () { + try { + await JSONRPCRequest(this, "grantTokenKyc", { + tokenId, + commonTransactionParams: { + signers: [tokenKycKey], + }, + }); + } catch (err: any) { + assert.equal(err.data.status, "INVALID_ACCOUNT_ID"); + return; + } + + assert.fail("Should throw an error"); + }); + + it("(#4) Grants KYC of a token to a deleted account", async function () { + await JSONRPCRequest(this, "deleteAccount", { + deleteAccountId: accountId, + transferAccountId: process.env.OPERATOR_ACCOUNT_ID, + commonTransactionParams: { + signers: [accountPrivateKey], + }, + }); + + try { + await JSONRPCRequest(this, "grantTokenKyc", { + tokenId, + accountId, + commonTransactionParams: { + signers: [tokenKycKey], + }, + }); + } catch (err: any) { + assert.equal(err.data.status, "ACCOUNT_DELETED"); + return; + } + + assert.fail("Should throw an error"); + }); + }); + + return Promise.resolve(); +});