fix and refactor dlt

This commit is contained in:
einhornimmond 2024-11-07 17:35:37 +01:00
parent 4dc4451b2f
commit 93883ae9f2
18 changed files with 321 additions and 224 deletions

View File

@ -61,6 +61,7 @@
"@types/jest": "^27.0.2",
"@types/lodash.clonedeep": "^4.5.6",
"@types/node": "^16.10.3",
"@types/node-fetch": "^2.6.11",
"@types/nodemailer": "^6.4.4",
"@types/sodium-native": "^2.3.5",
"@types/uuid": "^8.3.4",

View File

@ -1,16 +1,17 @@
import { Transaction as DbTransaction } from '@entity/Transaction'
import { User } from '@entity/User'
import { gql, GraphQLClient } from 'graphql-request'
// eslint-disable-next-line import/named, n/no-extraneous-import
import { FetchError } from 'node-fetch'
import { CONFIG } from '@/config'
import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId'
import { LogError } from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger'
import { AccountType } from './enum/AccountType'
import { TransactionDraft } from './model/TransactionDraft'
import { TransactionResult } from './model/TransactionResult'
import { UserAccountDraft } from './model/UserAccountDraft'
import { UserIdentifier } from './model/UserIdentifier'
const sendTransaction = gql`
mutation ($input: TransactionDraft!) {
@ -46,17 +47,6 @@ const registerAddress = gql`
}
`
// from ChatGPT
function getTransactionTypeString(id: TransactionTypeId): string {
const key = Object.keys(TransactionTypeId).find(
(key) => TransactionTypeId[key as keyof typeof TransactionTypeId] === id,
)
if (key === undefined) {
throw new LogError('invalid transaction type id: ' + id.toString())
}
return key
}
// Source: https://refactoring.guru/design-patterns/singleton/typescript/example
// and ../federation/client/FederationClientFactory.ts
/**
@ -109,92 +99,74 @@ export class DltConnectorClient {
return DltConnectorClient.instance
}
private getTransactionParams(input: DbTransaction | User): TransactionDraft | UserAccountDraft {
if (input instanceof DbTransaction) {
return new TransactionDraft(input)
} else if (input instanceof User) {
return new UserAccountDraft(input)
}
throw new LogError('transaction should be either Transaction or User Entity')
}
private handleTransactionResult(result: TransactionResult) {
if (result.error) {
throw new Error(result.error.message)
}
return result
}
private async sendTransaction(input: TransactionDraft) {
const {
data: { sendTransaction: result },
} = await this.client.rawRequest<{ sendTransaction: TransactionResult }>(sendTransaction, {
input,
})
return this.handleTransactionResult(result)
}
private async registerAddress(input: UserAccountDraft) {
const {
data: { registerAddress: result },
} = await this.client.rawRequest<{ registerAddress: TransactionResult }>(registerAddress, {
input,
})
return this.handleTransactionResult(result)
}
/**
* transmit transaction via dlt-connector to iota
* and update dltTransactionId of transaction in db with iota message id
*/
public async transmitTransaction(
transaction: DbTransaction,
transaction: DbTransaction | User,
): Promise<TransactionResult | undefined> {
// we don't need the receive transactions, there contain basically the same data as the send transactions
if ((transaction.typeId as TransactionTypeId) === TransactionTypeId.RECEIVE) {
if (
transaction instanceof DbTransaction &&
(transaction.typeId as TransactionTypeId) === TransactionTypeId.RECEIVE
) {
return
}
const typeString = getTransactionTypeString(transaction.typeId)
// no negative values in dlt connector, gradido concept don't use negative values so the code don't use it too
const amountString = transaction.amount.abs().toString()
const params = {
input: {
user: {
uuid: transaction.userGradidoID,
communityUuid: transaction.userCommunityUuid,
} as UserIdentifier,
linkedUser: {
uuid: transaction.linkedUserGradidoID,
communityUuid: transaction.linkedUserCommunityUuid,
} as UserIdentifier,
amount: amountString,
type: typeString,
createdAt: transaction.balanceDate.toISOString(),
targetDate: transaction.creationDate?.toISOString(),
},
}
const input = this.getTransactionParams(transaction)
try {
// TODO: add account nr for user after they have also more than one account in backend
logger.debug('transmit transaction to dlt connector', params)
const {
data: { sendTransaction: result },
} = await this.client.rawRequest<{ sendTransaction: TransactionResult }>(
sendTransaction,
params,
)
if (result.error) {
throw new Error(result.error.message)
logger.debug('transmit transaction or user to dlt connector', input)
if (input instanceof TransactionDraft) {
return await this.sendTransaction(input)
} else if (input instanceof UserAccountDraft) {
return await this.registerAddress(input)
} else {
throw new LogError('unhandled branch reached')
}
console.log(result)
return result
} catch (e) {
if (e instanceof Error) {
logger.error(e)
if (e instanceof FetchError) {
throw e
} else if (e instanceof Error) {
throw new LogError(`from dlt-connector: ${e.message}`)
} else {
throw new LogError('Exception sending transfer transaction to dlt-connector', e)
}
}
}
public async registerAddress(dbUser: User): Promise<TransactionResult | undefined> {
const params = {
input: {
user: {
uuid: dbUser.gradidoID,
communityUuid: dbUser.communityUuid,
accountNr: 1,
} as UserIdentifier,
createdAt: dbUser.createdAt.toISOString(),
accountType: AccountType.COMMUNITY_HUMAN,
} as UserAccountDraft,
}
try {
const {
data: { registerAddress: result },
} = await this.client.rawRequest<{ registerAddress: TransactionResult }>(
registerAddress,
params,
)
logger.info('send register address transaction to dlt-connector', {
params,
result,
})
if (result.error) {
throw new Error(result.error.message)
}
return result
} catch (e) {
if (e instanceof Error) {
throw new LogError(`from dlt-connector: ${e.message}`)
} else {
throw new LogError('Exception sending register address transaction to dlt-connector', e)
}
}
}
}

View File

@ -0,0 +1,38 @@
// https://www.npmjs.com/package/@apollo/protobufjs
import { Transaction } from '@entity/Transaction'
import { TransactionTypeId } from '@/graphql/enum/TransactionTypeId'
import { LogError } from '@/server/LogError'
import { UserIdentifier } from './UserIdentifier'
export class TransactionDraft {
user: UserIdentifier
linkedUser: UserIdentifier
amount: string
type: string
createdAt: string
// only for creation transactions
targetDate?: string
constructor(transaction: Transaction) {
if (
!transaction.linkedUserGradidoID ||
!transaction.linkedUserCommunityUuid ||
!transaction.userCommunityUuid
) {
throw new LogError(
`missing necessary field in transaction: ${transaction.id}, need linkedUserGradidoID, linkedUserCommunityUuid and userCommunityUuid`,
)
}
this.user = new UserIdentifier(transaction.userGradidoID, transaction.userCommunityUuid)
this.linkedUser = new UserIdentifier(
transaction.linkedUserGradidoID,
transaction.linkedUserCommunityUuid,
)
this.amount = transaction.amount.abs().toString()
this.type = TransactionTypeId[transaction.typeId]
this.createdAt = transaction.balanceDate.toISOString()
this.targetDate = transaction.creationDate?.toISOString()
}
}

View File

@ -1,9 +1,17 @@
import { User } from '@entity/User'
import { AccountType } from '@/apis/dltConnector/enum/AccountType'
import { UserIdentifier } from './UserIdentifier'
export interface UserAccountDraft {
export class UserAccountDraft {
user: UserIdentifier
createdAt: string
accountType: AccountType
constructor(user: User) {
this.user = new UserIdentifier(user.gradidoID, user.communityUuid)
this.createdAt = user.createdAt.toISOString()
this.accountType = AccountType.COMMUNITY_HUMAN
}
}

View File

@ -1,5 +1,11 @@
export interface UserIdentifier {
export class UserIdentifier {
uuid: string
communityUuid: string
accountNr?: number
constructor(uuid: string, communityUuid: string, accountNr?: number) {
this.uuid = uuid
this.communityUuid = communityUuid
this.accountNr = accountNr
}
}

View File

@ -0,0 +1,142 @@
import { DltTransaction } from '@entity/DltTransaction'
import { DltUser } from '@entity/DltUser'
import { Transaction } from '@entity/Transaction'
import { User } from '@entity/User'
// eslint-disable-next-line import/named, n/no-extraneous-import
import { FetchError } from 'node-fetch'
import { DltConnectorClient } from '@dltConnector/DltConnectorClient'
import { TransactionResult } from '@/apis/dltConnector/model/TransactionResult'
import { backendLogger as logger } from '@/server/logger'
import {
InterruptiveSleepManager,
TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY,
} from '@/util/InterruptiveSleepManager'
let isLoopRunning = true
export const stopSendTransactionsToDltConnector = (): void => {
isLoopRunning = false
}
function logTransactionResult(
type: 'dltUser' | 'dltTransaction',
data: { id: number; messageId: string; error: string | null },
): void {
if (data.error) {
logger.error(`Store ${type} with error: id=${data.id}, error=${data.error}`)
} else {
logger.info(`Store ${type}: messageId=${data.messageId}, id=${data.id}`)
}
}
async function saveTransactionResult(
pendingTransaction: User | Transaction,
messageId: string,
error: string | null,
): Promise<void> {
if (pendingTransaction instanceof User) {
const dltUser = DltUser.create()
dltUser.userId = pendingTransaction.id
dltUser.messageId = messageId
dltUser.error = error
await DltUser.save(dltUser)
logTransactionResult('dltUser', dltUser)
} else if (pendingTransaction instanceof Transaction) {
const dltTransaction = DltTransaction.create()
dltTransaction.transactionId = pendingTransaction.id
dltTransaction.messageId = messageId
dltTransaction.error = error
await DltTransaction.save(dltTransaction)
logTransactionResult('dltTransaction', dltTransaction)
}
}
async function findNextPendingTransaction(): Promise<Transaction | User | null> {
const lastTransactionPromise: Promise<Transaction | null> = Transaction.createQueryBuilder()
.leftJoin(DltTransaction, 'dltTransaction', 'Transaction.id = dltTransaction.transactionId')
.where('dltTransaction.transaction_id IS NULL')
// eslint-disable-next-line camelcase
.orderBy({ balance_date: 'ASC', Transaction_id: 'ASC' })
.limit(1)
.getOne()
const lastUserPromise: Promise<User | null> = User.createQueryBuilder()
.leftJoin(DltUser, 'dltUser', 'User.id = dltUser.userId')
.where('dltUser.user_id IS NULL')
// eslint-disable-next-line camelcase
.orderBy({ User_created_at: 'ASC', User_id: 'ASC' })
.limit(1)
.getOne()
const results = await Promise.all([lastTransactionPromise, lastUserPromise])
if (results[0] && results[1]) {
return results[0].balanceDate < results[1].createdAt ? results[0] : results[1]
} else if (results[0]) {
return results[0]
} else if (results[1]) {
return results[1]
}
return null
}
async function processPendingTransactions(dltConnector: DltConnectorClient): Promise<void> {
let pendingTransaction: Transaction | User | null = null
while ((pendingTransaction = await findNextPendingTransaction())) {
let result: TransactionResult | undefined
let messageId = ''
let error: string | null = null
try {
result = await dltConnector.transmitTransaction(pendingTransaction)
if (result?.succeed && result.recipe) {
messageId = result.recipe.messageIdHex
} else {
error = 'skipped'
}
} catch (e) {
if (e instanceof FetchError) {
throw e
}
error = e instanceof Error ? e.message : String(e)
}
await saveTransactionResult(pendingTransaction, messageId, error)
}
}
export async function sendTransactionsToDltConnector(): Promise<void> {
const dltConnector = DltConnectorClient.getInstance()
if (!dltConnector) {
logger.info('Sending to DltConnector currently not configured...')
isLoopRunning = false
return
}
logger.info('Starting sendTransactionsToDltConnector task')
// eslint-disable-next-line no-unmodified-loop-condition
while (isLoopRunning) {
try {
// return after no pending transactions are left
await processPendingTransactions(dltConnector)
await InterruptiveSleepManager.getInstance().sleep(
TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY,
1000,
)
} catch (e) {
// couldn't connect to dlt-connector? We wait
if (e instanceof FetchError) {
logger.error(`error connecting dlt-connector, wait 5 seconds before retry: ${String(e)}`)
await InterruptiveSleepManager.getInstance().sleep(
TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY,
5000,
)
} else {
logger.error(`Error while sending to DLT-connector or writing messageId`, e)
}
}
}
}

View File

@ -38,7 +38,7 @@ import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
import { fullName } from '@/util/utilities'
import { calculateBalance } from '@/util/validate'
import { sendTransactionsToDltConnector } from '../../tasks/sendTransactionsToDltConnector'
import { sendTransactionsToDltConnector } from '../../apis/dltConnector/sendTransactionsToDltConnector'
import { executeTransaction } from './TransactionResolver'
import { getUserCreation, validateContribution } from './util/creations'

View File

@ -1,7 +1,7 @@
import { CONFIG } from './config'
import { startValidateCommunities } from './federation/validateCommunities'
import { createServer } from './server/createServer'
import { sendTransactionsToDltConnector } from './tasks/sendTransactionsToDltConnector'
import { sendTransactionsToDltConnector } from './apis/dltConnector/sendTransactionsToDltConnector'
async function main() {
const { app } = await createServer()

View File

@ -1,129 +0,0 @@
import { DltTransaction } from '@entity/DltTransaction'
import { DltUser } from '@entity/DltUser'
import { Transaction } from '@entity/Transaction'
import { User } from '@entity/User'
import { DltConnectorClient } from '@dltConnector/DltConnectorClient'
import { backendLogger as logger } from '@/server/logger'
import {
InterruptiveSleepManager,
TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY,
} from '@/util/InterruptiveSleepManager'
let running = true
export const stopSendTransactionsToDltConnector = (): void => {
running = false
}
export async function sendTransactionsToDltConnector(): Promise<void> {
const dltConnector = DltConnectorClient.getInstance()
if (!dltConnector) {
logger.info('sending to DltConnector currently not configured...')
running = false
return
}
logger.info('start sendTransactionsToDltConnector task')
// eslint-disable-next-line no-unmodified-loop-condition
while (running) {
try {
// loop while work could be found
while (true) {
const pendingTransaction = await findNextPendingTransaction()
if (pendingTransaction instanceof User) {
const dltUser = DltUser.create()
dltUser.userId = pendingTransaction.id
try {
const result = await dltConnector.registerAddress(pendingTransaction)
if (result?.succeed && result.recipe) {
dltUser.messageId = result.recipe.messageIdHex
}
} catch (e) {
if (e instanceof Error) {
dltUser.error = e.message
} else if (typeof e === 'string') {
dltUser.error = e
}
}
// wait until saved, necessary before next call to findNextPendingTransaction
await DltUser.save(dltUser)
if (dltUser.messageId) {
logger.info('store dltUser: messageId=%s, id=%d', dltUser.messageId, dltUser.id)
} else {
logger.error('store dltUser with error: id=%d, error=%s', dltUser.id, dltUser.error)
}
} else if (pendingTransaction instanceof Transaction) {
const dltTransaction = DltTransaction.create()
dltTransaction.transactionId = pendingTransaction.id
try {
const result = await dltConnector.transmitTransaction(pendingTransaction)
if (result?.succeed && result.recipe) {
dltTransaction.messageId = result.recipe.messageIdHex
} else {
dltTransaction.error = 'skipped'
}
} catch (e) {
if (e instanceof Error) {
dltTransaction.error = e.message
} else if (typeof e === 'string') {
dltTransaction.error = e
}
}
// wait until saved, necessary before next call to findNextPendingTransaction
await DltTransaction.save(dltTransaction)
if (dltTransaction.messageId) {
logger.info(
'store dltTransaction: messageId=%s, id=%d',
dltTransaction.messageId,
dltTransaction.id,
)
} else {
logger.error(
'store dltTransaction with error: id=%d, error=%s',
dltTransaction.id,
dltTransaction.error,
)
}
} else {
break
}
}
await InterruptiveSleepManager.getInstance().sleep(
TRANSMIT_TO_IOTA_INTERRUPTIVE_SLEEP_KEY,
1000,
)
} catch (e) {
logger.error(`error while sending to dlt-connector or writing messageId`, e)
}
}
}
async function findNextPendingTransaction(): Promise<Transaction | User | null> {
const lastTransactionPromise: Promise<Transaction | null> = Transaction.createQueryBuilder()
.leftJoin(DltTransaction, 'dltTransaction', 'Transaction.id = dltTransaction.transactionId')
.where('dltTransaction.transaction_id IS NULL')
// eslint-disable-next-line camelcase
.orderBy({ balance_date: 'ASC', Transaction_id: 'ASC' })
.limit(1)
.getOne()
const lastUserPromise: Promise<User | null> = User.createQueryBuilder()
.leftJoin(DltUser, 'dltUser', 'User.id = dltUser.userId')
.where('dltUser.user_id IS NULL')
// eslint-disable-next-line camelcase
.orderBy({ User_created_at: 'ASC', User_id: 'ASC' })
.limit(1)
.getOne()
const results = await Promise.all([lastTransactionPromise, lastUserPromise])
if (results[0] && results[1]) {
return results[0].balanceDate < results[1].createdAt ? results[0] : results[1]
} else if (results[0]) {
return results[0]
} else if (results[1]) {
return results[1]
}
return null
}

View File

@ -1126,6 +1126,14 @@
dependencies:
"@types/node" "*"
"@types/node-fetch@^2.6.11":
version "2.6.11"
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.11.tgz#9b39b78665dae0e82a08f02f4967d62c66f95d24"
integrity sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==
dependencies:
"@types/node" "*"
form-data "^4.0.0"
"@types/node@*", "@types/node@^16.10.3":
version "16.10.3"
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.10.3.tgz#7a8f2838603ea314d1d22bb3171d899e15c57bd5"
@ -3417,6 +3425,15 @@ form-data@^3.0.0:
combined-stream "^1.0.8"
mime-types "^2.1.12"
form-data@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.1.tgz#ba1076daaaa5bfd7e99c1a6cb02aa0a5cff90d48"
integrity sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
mime-types "^2.1.12"
forwarded@0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"

View File

@ -1,9 +1,10 @@
// https://www.npmjs.com/package/@apollo/protobufjs
import { InputTransactionType } from '@enum/InputTransactionType'
import { isValidDateString, isValidNumberString } from '@validator/DateString'
import { IsEnum, IsObject, ValidateNested } from 'class-validator'
import { InputType, Field } from 'type-graphql'
import { InputTransactionType } from '@enum/InputTransactionType'
import { isValidDateString, isValidNumberString } from '@validator/DateString'
import { UserIdentifier } from './UserIdentifier'
@InputType()

View File

@ -1,9 +1,10 @@
// https://www.npmjs.com/package/@apollo/protobufjs
import { isValidDateString } from '@validator/DateString'
import { IsEnum, IsObject, ValidateNested } from 'class-validator'
import { InputType, Field } from 'type-graphql'
import { isValidDateString } from '@validator/DateString'
import { AccountType } from '@/graphql/enum/AccountType'
import { UserIdentifier } from './UserIdentifier'

View File

@ -19,7 +19,7 @@ export class TransactionResult {
@Field(() => TransactionError, { nullable: true })
error?: TransactionError
// if no error happend, the message id of the iota transaction
// if no error happened, the message id of the iota transaction
@Field(() => TransactionRecipe, { nullable: true })
recipe?: TransactionRecipe

View File

@ -1,6 +1,7 @@
import { TransactionDraft } from '@input/TransactionDraft'
import { Resolver, Arg, Mutation } from 'type-graphql'
import { TransactionDraft } from '@input/TransactionDraft'
import { SendToIotaContext } from '@/interactions/sendToIota/SendToIota.context'
import { TransactionError } from '../model/TransactionError'

View File

@ -38,4 +38,4 @@ export function isValidNumberString(validationOptions?: ValidationOptions) {
},
})
}
}
}

View File

@ -12,7 +12,7 @@ import { UserKeyPairRole } from './UserKeyPair.role'
/**
* @DCI-Context
* Context for calculating key pair for signing transactions
* Context for calculating key pair for signing transactions
*/
export async function KeyPairCalculation(input: UserIdentifier | string): Promise<KeyPairEd25519> {
const cache = KeyPairCacheManager.getInstance()

View File

@ -1082,7 +1082,7 @@
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690"
integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==
"@types/node-fetch@^2.6.1":
"@types/node-fetch@^2.6.1", "@types/node-fetch@^2.6.11":
version "2.6.11"
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.11.tgz#9b39b78665dae0e82a08f02f4967d62c66f95d24"
integrity sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==
@ -2112,6 +2112,11 @@ cssstyle@^2.3.0:
dependencies:
cssom "~0.3.6"
data-uri-to-buffer@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e"
integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==
data-urls@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-2.0.0.tgz#156485a72963a970f5d5821aaf642bef2bf2db9b"
@ -2931,6 +2936,14 @@ fb-watchman@^2.0.0:
dependencies:
bser "2.1.1"
fetch-blob@^3.1.2, fetch-blob@^3.1.4:
version "3.2.0"
resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9"
integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==
dependencies:
node-domexception "^1.0.0"
web-streams-polyfill "^3.0.3"
figures@^3.0.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af"
@ -3050,6 +3063,13 @@ form-data@^4.0.0:
combined-stream "^1.0.8"
mime-types "^2.1.12"
formdata-polyfill@^4.0.10:
version "4.0.10"
resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423"
integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==
dependencies:
fetch-blob "^3.1.2"
forwarded@0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
@ -4670,6 +4690,11 @@ node-api-headers@^1.1.0:
resolved "https://registry.yarnpkg.com/node-api-headers/-/node-api-headers-1.3.0.tgz#bb32c6b3e33fb0004bd93c66787bf00998c834ea"
integrity sha512-8Bviwtw4jNhv0B2qDjj4M5e6GyAuGtxsmZTrFJu3S3Z0+oHwIgSUdIKkKJmZd+EbMo7g3v4PLBbrjxwmZOqMBg==
node-domexception@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5"
integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==
node-fetch@^2.6.12, node-fetch@^2.6.7:
version "2.7.0"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d"
@ -4677,6 +4702,15 @@ node-fetch@^2.6.12, node-fetch@^2.6.7:
dependencies:
whatwg-url "^5.0.0"
node-fetch@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.2.tgz#d1e889bacdf733b4ff3b2b243eb7a12866a0b78b"
integrity sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==
dependencies:
data-uri-to-buffer "^4.0.0"
fetch-blob "^3.1.4"
formdata-polyfill "^4.0.10"
node-gyp-build@^4.8.1:
version "4.8.2"
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.2.tgz#4f802b71c1ab2ca16af830e6c1ea7dd1ad9496fa"
@ -6265,6 +6299,11 @@ walker@^1.0.7:
dependencies:
makeerror "1.0.12"
web-streams-polyfill@^3.0.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b"
integrity sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==
webidl-conversions@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871"