Skip to content

Commit 0cc5d5a

Browse files
OshanMPyatakov
authored andcommitted
feat: 5800 Add API Endpoints for Token Transfers
* - Added token transfer APIs (sync and async) under /tokens controller path (same as token associate API patterns) - Added service functions for token transfer - Added support for both FT and NFT token transfers - NFT transfers support with and without serial numbers mentioned - Added new TaskAction typer for token transfers - Added MessageAPI enum for token transfer functions (sync and async) - Minor lint style fixes - Fixed conflicts * - Added batching for transactions exceeding maximum batch size for NFT (async) - Restricted sync NFT transaction calls to limit amount to less than the batch size * - Removed duplicated code and added common transferTokenCore() function * - Moved self-transfer guard before vault call * - Rounded the token value * - Changed retry attempts to 1 from 3 * - Removed Promise.all() on NFT transfer and made it sequencial * - Added TransferTokenDTO and fixed the API bodies --------- Signed-off-by: OshanM <oshanm@xeptagon.com>
1 parent b0c7cd3 commit 0cc5d5a

7 files changed

Lines changed: 455 additions & 15 deletions

File tree

api-gateway/src/api/service/tokens.ts

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { IAuthUser, PinoLogger, RunFunctionAsync } from '@guardian/common';
44
import { Body, Controller, Delete, Get, HttpCode, HttpException, HttpStatus, Param, Post, Put, Query, Req, Response, Version } from '@nestjs/common';
55
import { AuthUser, Auth } from '#auth';
66
import { ApiAcceptedResponse, ApiBody, ApiCreatedResponse, ApiExtraModels, ApiForbiddenResponse, ApiInternalServerErrorResponse, ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiParam, ApiQuery, ApiTags, ApiUnprocessableEntityResponse } from '@nestjs/swagger';
7-
import { Examples, InternalServerErrorDTO, ObjectExamples, TaskDTO, TokenDTO, TokenInfoDTO, UnprocessableEntityErrorDTO, pageHeader } from '#middlewares';
7+
import { Examples, InternalServerErrorDTO, ObjectExamples, TaskDTO, TokenDTO, TokenInfoDTO, TransferTokenDTO, UnprocessableEntityErrorDTO, pageHeader } from '#middlewares';
88
import { TOKEN_REQUIRED_PROPS } from '#constants';
99

1010
/**
@@ -1116,6 +1116,111 @@ export class TokensApi {
11161116
return task;
11171117
}
11181118

1119+
/**
1120+
* Transfer token
1121+
*/
1122+
@Post('/:tokenId/transfer')
1123+
@Auth(
1124+
Permissions.TOKENS_TOKEN_EXECUTE,
1125+
)
1126+
@ApiOperation({
1127+
summary: 'Transfers tokens from the authenticated user to the target account.',
1128+
description: 'Transfers fungible or non-fungible tokens from the authenticated user\'s Hedera account to the specified target account. For FT, specify amount. For NFT, specify serialNumbers or amount (picks from end).',
1129+
})
1130+
@ApiParam({
1131+
name: 'tokenId',
1132+
type: String,
1133+
description: 'Token ID',
1134+
required: true,
1135+
example: Examples.DB_ID
1136+
})
1137+
@ApiBody({ type: TransferTokenDTO })
1138+
@ApiOkResponse({
1139+
description: 'Successful operation.',
1140+
})
1141+
@ApiInternalServerErrorResponse({
1142+
description: 'Internal server error.',
1143+
type: InternalServerErrorDTO
1144+
})
1145+
@ApiExtraModels(InternalServerErrorDTO)
1146+
@HttpCode(HttpStatus.OK)
1147+
async transferToken(
1148+
@AuthUser() user: IAuthUser,
1149+
@Param('tokenId') tokenId: string,
1150+
@Body() body: TransferTokenDTO
1151+
): Promise<any> {
1152+
try {
1153+
if (!user.did) {
1154+
throw new HttpException('User is not registered.', HttpStatus.UNPROCESSABLE_ENTITY);
1155+
}
1156+
const owner = new EntityOwner(user);
1157+
const guardians = new Guardians();
1158+
return await guardians.transferToken(tokenId, body, owner);
1159+
} catch (error) {
1160+
await this.logger.error(error, ['API_GATEWAY'], user.id);
1161+
if (error?.message?.toLowerCase().includes('user not found')) {
1162+
throw new HttpException('User not found.', HttpStatus.NOT_FOUND);
1163+
}
1164+
if (error?.message?.toLowerCase().includes('token not found')) {
1165+
throw new HttpException('Token does not exist.', HttpStatus.NOT_FOUND);
1166+
}
1167+
if (error instanceof HttpException) {
1168+
throw error;
1169+
}
1170+
throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR);
1171+
}
1172+
}
1173+
1174+
/**
1175+
* Transfer token (async)
1176+
*/
1177+
@Post('/push/:tokenId/transfer')
1178+
@Auth(
1179+
Permissions.TOKENS_TOKEN_EXECUTE,
1180+
)
1181+
@ApiOperation({
1182+
summary: 'Transfers tokens from the authenticated user to the target account.',
1183+
description: 'Transfers fungible or non-fungible tokens asynchronously. Returns a task ID for tracking.',
1184+
})
1185+
@ApiParam({
1186+
name: 'tokenId',
1187+
type: String,
1188+
description: 'Token ID',
1189+
required: true,
1190+
example: Examples.DB_ID
1191+
})
1192+
@ApiBody({ type: TransferTokenDTO })
1193+
@ApiOkResponse({
1194+
description: 'Successful operation.',
1195+
type: TaskDTO
1196+
})
1197+
@ApiInternalServerErrorResponse({
1198+
description: 'Internal server error.',
1199+
type: InternalServerErrorDTO
1200+
})
1201+
@ApiExtraModels(TaskDTO, InternalServerErrorDTO)
1202+
@HttpCode(HttpStatus.ACCEPTED)
1203+
async transferTokenAsync(
1204+
@AuthUser() user: IAuthUser,
1205+
@Param('tokenId') tokenId: string,
1206+
@Body() body: TransferTokenDTO
1207+
): Promise<TaskDTO> {
1208+
if (!user.did) {
1209+
throw new HttpException('User is not registered.', HttpStatus.UNPROCESSABLE_ENTITY);
1210+
}
1211+
const owner = new EntityOwner(user);
1212+
const taskManager = new TaskManager();
1213+
const task = taskManager.start(TaskAction.TRANSFER_TOKEN, user.id);
1214+
RunFunctionAsync<ServiceError>(async () => {
1215+
const guardians = new Guardians();
1216+
await guardians.transferTokenAsync(tokenId, body, owner, task);
1217+
}, async (error) => {
1218+
await this.logger.error(error, ['API_GATEWAY'], user.id);
1219+
taskManager.addError(task.taskId, { code: error.code || 500, message: error.message });
1220+
});
1221+
return task;
1222+
}
1223+
11191224
/**
11201225
* KYC
11211226
*/

api-gateway/src/helpers/guardians.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,55 @@ export class Guardians extends NatsService {
476476
});
477477
}
478478

479+
/**
480+
* Transfer token
481+
* @param tokenId
482+
* @param body
483+
* @param owner
484+
*/
485+
public async transferToken(
486+
tokenId: string,
487+
body: {
488+
targetAccount: string,
489+
amount?: number,
490+
serialNumbers?: number[],
491+
memo?: string
492+
},
493+
owner: IOwner
494+
): Promise<any> {
495+
return await this.sendMessage(MessageAPI.TRANSFER_TOKEN, {
496+
tokenId,
497+
body,
498+
owner,
499+
});
500+
}
501+
502+
/**
503+
* Async transfer token
504+
* @param tokenId
505+
* @param body
506+
* @param owner
507+
* @param task
508+
*/
509+
public async transferTokenAsync(
510+
tokenId: string,
511+
body: {
512+
targetAccount: string,
513+
amount?: number,
514+
serialNumbers?: number[],
515+
memo?: string
516+
},
517+
owner: IOwner,
518+
task: NewTask
519+
): Promise<NewTask> {
520+
return await this.sendMessage(MessageAPI.TRANSFER_TOKEN_ASYNC, {
521+
tokenId,
522+
body,
523+
owner,
524+
task,
525+
});
526+
}
527+
479528
/**
480529
* Get token info
481530
* @param tokenId

api-gateway/src/middlewares/validation/schemas/token.dto.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ApiProperty } from '@nestjs/swagger';
2+
import { ArrayMinSize, IsArray, IsInt, IsNotEmpty, IsNumber, IsOptional, IsPositive, IsString, Min, ValidateIf } from 'class-validator';
23
import { Examples } from '../examples.js';
34

45
export class TokenDTO {
@@ -205,3 +206,29 @@ export class TokenInfoDTO {
205206
})
206207
enableWipe?: boolean;
207208
}
209+
210+
export class TransferTokenDTO {
211+
@ApiProperty({ type: String, description: 'Target Hedera account ID', example: '0.0.12345' })
212+
@IsString()
213+
@IsNotEmpty()
214+
targetAccount: string;
215+
216+
@ApiProperty({ type: Number, description: 'Amount (FT) or serial count to pick (NFT); must be > 0', required: false, example: 10 })
217+
@ValidateIf(o => !o.serialNumbers?.length)
218+
@IsNumber()
219+
@IsPositive()
220+
amount?: number;
221+
222+
@ApiProperty({ type: [Number], description: 'Specific NFT serial numbers to transfer; positive integers', required: false, example: [1, 2, 3] })
223+
@IsOptional()
224+
@IsArray()
225+
@IsInt({ each: true })
226+
@Min(1, { each: true })
227+
@ArrayMinSize(1)
228+
serialNumbers?: number[];
229+
230+
@ApiProperty({ type: String, description: 'Optional transaction memo', required: false })
231+
@IsOptional()
232+
@IsString()
233+
memo?: string;
234+
}

configs/.env..guardian.system

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ HEDERA_NET="testnet" # valid options: mainnet, testnet, previewnet, local-node
1313
PREUSED_HEDERA_NET="testnet"
1414

1515
# TESTNET
16-
OPERATOR_ID="..."
17-
OPERATOR_KEY="..."
16+
OPERATOR_ID="0.0.8482539"
17+
OPERATOR_KEY="3030020100300706052b8104000a0422042033694cd14257b371ee040d86246794497831aa892986a0283563c95579487cd6"
1818
INITIALIZATION_TOPIC_ID="0.0.1960"
1919
SR_INITIAL_PASSWORD="..."
2020

@@ -41,12 +41,14 @@ OVERRIDE_HEDERA_CONSENSUS_NODES={"0.testnet.hedera.com:50211":"0.0.3"} # object
4141
OVERRIDE_HEDERA_MIRROR_NODES=["testnet.mirrornode.hedera.com:443"] # array of base URLs: ["http://10.0.2.15:8090", "http://10.0.2.15:8090", ...]
4242
OVERRIDE_HEDERA_MIRROR_NODES_BASE_API="/api/v1"
4343

44+
4445
# MAX_TRANSACTION_FEE="10"
4546

4647
# ADDRESSING / SERVICES
4748
# ----------------------
48-
JWT_PRIVATE_KEY="..."
49-
JWT_PUBLIC_KEY="..."
49+
50+
JWT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nMIICWwIBAAKBgFLH4kaMzvFfR6HHx3fSeGVGnmI6az7ryV06+EWGDL3LMX7XP9mP\nvaqvW+Vj2oTKohB1jrfA12xSgsJzrX9geKLjy/avXSBtUUFfAsxoqB0RTlS1hLXL\ntuxJZzLxBZ+es8KwETxSvhkl2ft8D7nCA7Hhl+ieUUU4Pp8RE3FpSOSVAgMBAAEC\ngYA4NHDk99vWNJv9MxiyuVt3X/BPv1xrt0ncHBrPkYl7H8g2V82JWTgs+K1LXXXT\nrYVF6ZuCFdPuP0N65XHKR2UAZO0FRrrnNHfTuRqStpSFjKfY+gU5KeakcNNgZ3bH\nsPbhSWDmeJduGUZoeyD4PsgOuU98go2ZzbLZuLdS9jRuAQJBAKNfDgcqY8dSW+VF\ns88N8FL5Y3frz0we+5bNxODFIgjgBWXGpq2UeAoEG1C7RuobAKbWkf6xKzyaEs6w\nfj196RUCQQCBt1BRzqG0PYwvsvXDdg4y4LZupGH5CfMJWNrPszJo4raqslO6W6NN\naeDIIXM69Emv5o6M1bam/gH3apYTVu2BAkEAgehnDjmd5/RMkI6RgXbTABfBCa3g\nQ+ZrJqpvAFChVK9We2ywQ1zI6yYC1npk1GkuZnw+0WjuOZ1GkUZ0y87gVQJACPbT\nHSQrdpo+QtmaNEzHRGrVEKZZgu1WF1JFpM7cb/Ui7H7wXFOS4HmtqAbvwKY7JWXI\neiEWw0mQdzmtb161gQJARGuEUqcmt9mZZoFiITpLwW/gNt7xqEeVy3kAFwa2iFyj\ncT0lsNwFzE2BdrlpR3+wdWaJ+WjYjrQBSaGMmobtrA==\n-----END RSA PRIVATE KEY-----"
51+
JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nMIGeMA0GCSqGSIb3DQEBAQUAA4GMADCBiAKBgFLH4kaMzvFfR6HHx3fSeGVGnmI6\naz7ryV06+EWGDL3LMX7XP9mPvaqvW+Vj2oTKohB1jrfA12xSgsJzrX9geKLjy/av\nXSBtUUFfAsxoqB0RTlS1hLXLtuxJZzLxBZ+es8KwETxSvhkl2ft8D7nCA7Hhl+ie\nUUU4Pp8RE3FpSOSVAgMBAAE=\n-----END PUBLIC KEY-----"
5052
MQ_ADDRESS="message-broker"
5153
MQ_MAX_PAYLOAD="1048576"
5254

@@ -60,11 +62,11 @@ TASK_TIMEOUT="300"
6062
REFRESH_INTERVAL="60"
6163

6264
IPFS_TIMEOUT="720" # seconds
63-
IPFS_PROVIDER="web3storage" # valid options: 'filebase', 'web3storage', 'local'
64-
IPFS_STORAGE_KEY="..." # only valid for web3storage provider; ignored in other cases
65-
IPFS_STORAGE_PROOF="..." # only valid for web3storage provider; ignored in other cases
66-
IPFS_PUBLIC_GATEWAY='https://{cid}.ipfs.w3s.link' # use this for public providers (filebase, web3storage, etc.)
67-
#IPFS_PUBLIC_GATEWAY='http://ipfs-node:8080/ipfs/{cid}' # use this for local provider
65+
IPFS_PROVIDER="local" # valid options: 'filebase', 'web3storage', 'local'
66+
IPFS_STORAGE_KEY="MgCbips6BrwUVX29/AI6OiFF9GUUIsBLp2IkZxjvSJD3f3+0BHSDMaF1YREFkQQ/K9bpn/5/aQCfHT4uJ1kYZ3GXPEUw=" # only valid for web3storage provider; ignored in other cases
67+
IPFS_STORAGE_PROOF="mAYIEAO4ROqJlcm9vdHOB2CpYJQABcRIgFFfOthryzfMbaVnOHZZOSLcZUPh6gCHK+OJyV4t+hOxndmVyc2lvbgHUAgFxEiCYAtkNFCeaicC0B8IsMVq05T8vu8M77Bzz4WtM2QfZRqhhc1hE7aEDQP9IoyRYRgTNr62qKqqw6p9fBymgMP1NTG7hBgBhfODK2NI10lSMXs8CxkPmWNzF2wtXzZVAOCibUh997CeCpwNhdmUwLjkuMWNhdHSBomNjYW5hKmR3aXRoeDhkaWQ6a2V5Ono2TWtvaDQ2RkNBdnJMcGk4WHl5cWdCUG1NVmIyZkpBVXk4VzU2YktLN1NORHhaQWNhdWRYLZ0abWFpbHRvOmVudmlzaW9uYmxvY2tjaGFpbi5jb206YWxleC5weWF0YWtvdmNleHD2Y2ZjdIGhZXNwYWNlomRuYW1laGd1YXJkaWFuZmFjY2Vzc6FkdHlwZWZwdWJsaWNjaXNzWCLtAYlBzAXtCgD4zxjdxdIHKiEbmtQqbdxM7pJO8Q2BOO9VY3ByZoDQAgFxEiDY64mqHb+a0dIKtVoym/tul6rFITNnqnjWbSknqPAAsahhc0SAoAMAYXZlMC45LjFjYXR0gaJjY2FuYSpkd2l0aGZ1Y2FuOipjYXVkWCLtASERq7XJwRXBrv/46B5x9Uq5e+BDS7N6vqJ+Wm82CL0pY2V4cPZjZmN0gaJuYWNjZXNzL2NvbmZpcm3YKlglAAFxEiDTLleW0qs7wWq/Z6RNPp4o4D2IxMStX+//5sFG2ckjZG5hY2Nlc3MvcmVxdWVzdNgqWCUAAXESIMhIhyarRFICo4QPDdOKDO8BNwHjXNTGaCqy0eHh4vEaY2lzc1gtnRptYWlsdG86ZW52aXNpb25ibG9ja2NoYWluLmNvbTphbGV4LnB5YXRha292Y3ByZoHYKlglAAFxEiCYAtkNFCeaicC0B8IsMVq05T8vu8M77Bzz4WtM2QfZRqcDAXESIAHN3A5bK5uArbsqBobEv1tdR4mok9Sd9kyxJHJEaqh2qGFzWETtoQNAYs0zUoxUiMTk7f/vy3M9zt9YepNy32hzTWMSudV9/W9Sob6zGsppV/h27zATBiGyI4FQq9qS9Fx+CN9OFBnIDGF2ZTAuOS4xY2F0dIGjYm5ioWVwcm9vZtgqWCUAAXESINjriaodv5rR0gq1WjKb+26XqsUhM2eqeNZtKSeo8ACxY2Nhbmt1Y2FuL2F0dGVzdGR3aXRoeBtkaWQ6d2ViOnVwLnN0b3JhY2hhLm5ldHdvcmtjYXVkWCLtASERq7XJwRXBrv/46B5x9Uq5e+BDS7N6vqJ+Wm82CL0pY2V4cPZjZmN0gaJuYWNjZXNzL2NvbmZpcm3YKlglAAFxEiDTLleW0qs7wWq/Z6RNPp4o4D2IxMStX+//5sFG2ckjZG5hY2Nlc3MvcmVxdWVzdNgqWCUAAXESIMhIhyarRFICo4QPDdOKDO8BNwHjXNTGaCqy0eHh4vEaY2lzc1gZnRp3ZWI6dXAuc3RvcmFjaGEubmV0d29ya2NwcmaAhggBcRIgddUIpAvhN2BjhQMSqBBXKI43WcqfV5rRKNM+kv/VrqmoYXNYRO2hA0B79mAftjYykI9kjXzCbBe69dtsPFko/FeMDXoN1P6+R3/GBdHFRFEaWVODZx+TiHubhc5RFiqMSjbltZX+JeUCYXZlMC45LjFjYXR0iaJjY2FuaGFzc2VydC8qZHdpdGh4OGRpZDprZXk6ejZNa29oNDZGQ0F2ckxwaThYeXlxZ0JQbU1WYjJmSkFVeThXNTZiS0s3U05EeFpBomNjYW5nc3BhY2UvKmR3aXRoeDhkaWQ6a2V5Ono2TWtvaDQ2RkNBdnJMcGk4WHl5cWdCUG1NVmIyZkpBVXk4VzU2YktLN1NORHhaQaJjY2FuZmJsb2IvKmR3aXRoeDhkaWQ6a2V5Ono2TWtvaDQ2RkNBdnJMcGk4WHl5cWdCUG1NVmIyZkpBVXk4VzU2YktLN1NORHhaQaJjY2FuZ2luZGV4Lypkd2l0aHg4ZGlkOmtleTp6Nk1rb2g0NkZDQXZyTHBpOFh5eXFnQlBtTVZiMmZKQVV5OFc1NmJLSzdTTkR4WkGiY2NhbmdzdG9yZS8qZHdpdGh4OGRpZDprZXk6ejZNa29oNDZGQ0F2ckxwaThYeXlxZ0JQbU1WYjJmSkFVeThXNTZiS0s3U05EeFpBomNjYW5odXBsb2FkLypkd2l0aHg4ZGlkOmtleTp6Nk1rb2g0NkZDQXZyTHBpOFh5eXFnQlBtTVZiMmZKQVV5OFc1NmJLSzdTTkR4WkGiY2NhbmhhY2Nlc3MvKmR3aXRoeDhkaWQ6a2V5Ono2TWtvaDQ2RkNBdnJMcGk4WHl5cWdCUG1NVmIyZkpBVXk4VzU2YktLN1NORHhaQaJjY2FuamZpbGVjb2luLypkd2l0aHg4ZGlkOmtleTp6Nk1rb2g0NkZDQXZyTHBpOFh5eXFnQlBtTVZiMmZKQVV5OFc1NmJLSzdTTkR4WkGiY2Nhbmd1c2FnZS8qZHdpdGh4OGRpZDprZXk6ejZNa29oNDZGQ0F2ckxwaThYeXlxZ0JQbU1WYjJmSkFVeThXNTZiS0s3U05EeFpBY2F1ZFgi7QEdIMxoXVhEQWRBD8r1umf/n9pAJ8dPi4nWRhncZc8RTGNleHD2Y2ZjdIGhZXNwYWNlomRuYW1laGd1YXJkaWFuZmFjY2Vzc6FkdHlwZWZwdWJsaWNjaXNzWCLtASERq7XJwRXBrv/46B5x9Uq5e+BDS7N6vqJ+Wm82CL0pY3ByZoLYKlglAAFxEiDY64mqHb+a0dIKtVoym/tul6rFITNnqnjWbSknqPAAsdgqWCUAAXESIAHN3A5bK5uArbsqBobEv1tdR4mok9Sd9kyxJHJEaqh2WQFxEiAUV862GvLN8xtpWc4dlk5ItxlQ+HqAIcr44nJXi36E7KFqdWNhbkAwLjkuMdgqWCUAAXESIHXVCKQL4TdgY4UDEqgQVyiON1nKn1ea0SjTPpL/1a6p" # only valid for web3storage provider; ignored in other cases
68+
#IPFS_PUBLIC_GATEWAY='https://{cid}.ipfs.w3s.link' # use this for public providers (filebase, web3storage, etc.)
69+
IPFS_PUBLIC_GATEWAY='http://ipfs-node:8080/ipfs/{cid}' # use this for local provider
6870
IPFS_NODE_ADDRESS="http://ipfs-node:5001" # only valid for local provider; ignored in other cases
6971
ANALYTICS_SERVICE="http://indexer-api-gateway:3021"
7072
#ANALYTICS_SERVICE_TOKEN="" # mandatory to be able to use the MGS indexer
@@ -86,6 +88,7 @@ MULTI_POLICY_SCHEDULER="0 0 * * *"
8688
# FEATURES
8789
# --------------
8890
BBS_SIGNATURES_MODE="WASM"
91+
# PYTHON_SANDBOX_MODE="pyodide" # "pyodide" (default) or "docker"
8992
# IMPORT_KEYS_FROM_DB=1
9093
# MQ_MESSAGE_CHUNK=5000000
9194
# RAW_REQUEST_LIMIT="1gb"
@@ -167,10 +170,11 @@ BLOCK_PRIVATE_IP="false"
167170
#Service secrets for auth between services
168171
# SERVICE_JWT_SECRET_KEY_ALL and SERVICE_JWT_PUBLIC_KEY_ALL are used for local development only
169172
# Recommended to create separate key pairs for each service
170-
SERVICE_JWT_PUBLIC_KEY_ALL="..."
171-
SERVICE_JWT_SECRET_KEY_ALL="..."
173+
SERVICE_JWT_SECRET_KEY_ALL="-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCk3t8tlNZ785e0\nJ+b5vvkPh0LJgbnYOFFIBksN80wFNxfjKg/T9bJ+RJZHSifoYnMd9yqTDHUndsFm\nJUL2So/Lt1p54uyYN2G4ERrU+MI2bcxc1kPa+F5hu8g8shgIQTE5vc6TtM6Ifw8R\njPmFEojCa7QtbePTnttsUdZOLl7vEyjmCW+L6vBHM0oL+D8MjxT48JH5hErtrnTp\nX4cBaZEq9x9hm6MKSWXoarMH74VpaBVfNuPRB7aqquT4g0+FWQ4eTltFlsPoQm8P\nMSxpX/h24wxUe/reYOCeVmkQTTSALrfeeoUlzT5UQNN36garSZBH9TOGXohn7O8+\nfep9augfAgMBAAECggEACACB4Cz6p1kgfPp4z+37GaFXgP8+XWZdBOa6oCqK/y/N\nhYAw9M6BLiFhvS5gtyXcGia5FsUChKVTaAPuoZSR28cs6YCK5Ne/SEHwRZ5DPYVh\nEgUblaumAmQCU8vTCxza6PII7jYBgNzQhuoK4Ztk2YbaX7VshYS5d/hAM1rgebBQ\n7ZRL4tQt02jD/XChzHibJtqLpM+5IoTIjE331vcycpadsg43UXQINO9I9EMl4TGb\nrEfGoTV0xiuPMJXCGt2XBL3tPkcWediT1KN5ZpvuIXu5smYMI8Fi+xUm38QfRJIE\n482cV8/dAEqdhnVrUG+yDkhbry31vRE2BsIxe+sJaQKBgQDRGNC+toA7wxYtu+vl\n/ppeHNCMdq/XEJ6iqKUYPHSTiLDsxvuij2WYm/t62p0/MhFm7fs2KhAv34tx6dOy\nqzpyFexLWG5kG3FMjNrKRclLv6HKv/sxpoYFkv08Rsv0wnWyEMrh2SscLLa0E64q\npFrOkpoUEN0yEaAYyOdfrrKtZwKBgQDJ2mcGPerjHxnz/PDRNLc3U2yBqogbNNxj\nTR8GDdt8JGG5/OMlwaVIBeER/AgEpkHcVuf79mVRn/iWf4M5mCKrdWaA2mRktv0T\nw3aPQYPBaBzFF1jkAfvEZ46C3omu9xjFzsXFqHJecyt9kCC6fbku3uRA6ODIOv50\nPzD7LaWEiQKBgQCVivDoJK/rjefTx527/O48NtF0VAIIhytdW83PWpVpWo5mmR0o\nsvPPRGeEAswJgW5ute8/Wq/+/RrG2pt8IfgH1eQMMu+oivPp8qcbmPORSDmXPtyR\nMu6RGAIi1ONTZqw0MMxY4C9z1ArLGXQrrSYArVqi1TjNcUuVzkGj7dZ+KwKBgDRW\nlfoPWfU0HkWeY07LjWoiDnN8pTfwt+hjmdS3CR8iS9iu0rL6iAGpzJceM3IJLfCU\n9Cfn1pOYmBtlyr/HS84Lbd2hQwC+VdanCvnQMfqXJUaRbDIKtZ5Sf6g9TZP5bAn2\nOF+s8qK82B0BnwrcCIU3tBWEjKw+Z7X6oJewWUeJAoGAFA9kJlRo8yFXb4GELlbd\nksuxdMQ5qsAoS4fQKEqGF/WbfvANh6lsUVpIsbbud6vftZTLmhGx2vkzjAvdIFDR\nUL5sJhUfvqgrfiLuPsW3Ik+MaR0rMIk93UztXvqGYW+7bQgblhujYHowh5iqoVPZ\nhZlUAa8O+l53H99w1y+n0+I=\n-----END PRIVATE KEY-----"
174+
SERVICE_JWT_PUBLIC_KEY_ALL="-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApN7fLZTWe/OXtCfm+b75\nD4dCyYG52DhRSAZLDfNMBTcX4yoP0/WyfkSWR0on6GJzHfcqkwx1J3bBZiVC9kqP\ny7daeeLsmDdhuBEa1PjCNm3MXNZD2vheYbvIPLIYCEExOb3Ok7TOiH8PEYz5hRKI\nwmu0LW3j057bbFHWTi5e7xMo5glvi+rwRzNKC/g/DI8U+PCR+YRK7a506V+HAWmR\nKvcfYZujCkll6GqzB++FaWgVXzbj0Qe2qqrk+INPhVkOHk5bRZbD6EJvDzEsaV/4\nduMMVHv63mDgnlZpEE00gC633nqFJc0+VEDTd+oGq0mQR/Uzhl6IZ+zvPn3qfWro\nHwIDAQAB\n-----END PUBLIC KEY-----"
175+
172176
# set QM_VERIFICATION to false to disable message verification and signing
173-
# QM_VERIFICATION=false
177+
QM_VERIFICATION=false
174178

175179
#Integrations tokens
176180
KANOP_IO_AUTH_TOKEN="..."

0 commit comments

Comments
 (0)