diff --git a/src/common/factory.ts b/src/common/factory.ts
index f8e0cb94..2522a942 100644
--- a/src/common/factory.ts
+++ b/src/common/factory.ts
@@ -15,20 +15,33 @@
* along with this program. If not, see .
*/
import { TurboUnauthenticatedConfiguration } from '../types.js';
+import { TurboWinstonLogger } from './logger.js';
import { TurboUnauthenticatedPaymentService } from './payment.js';
import { TurboUnauthenticatedClient } from './turbo.js';
import { TurboUnauthenticatedUploadService } from './upload.js';
export class TurboBaseFactory {
+ protected static logger = new TurboWinstonLogger();
+
+ static setLogLevel(level: string) {
+ this.logger.setLogLevel(level);
+ }
+
+ static setLogFormat(format: string) {
+ this.logger.setLogFormat(format);
+ }
+
static unauthenticated({
paymentServiceConfig = {},
uploadServiceConfig = {},
}: TurboUnauthenticatedConfiguration = {}) {
const paymentService = new TurboUnauthenticatedPaymentService({
...paymentServiceConfig,
+ logger: this.logger,
});
const uploadService = new TurboUnauthenticatedUploadService({
...uploadServiceConfig,
+ logger: this.logger,
});
return new TurboUnauthenticatedClient({
uploadService,
diff --git a/src/common/http.ts b/src/common/http.ts
index 8e1242e8..360770e6 100644
--- a/src/common/http.ts
+++ b/src/common/http.ts
@@ -21,6 +21,7 @@ import { ReadableStream } from 'stream/web';
import {
TurboHTTPServiceInterface,
+ TurboLogger,
TurboSignedRequestHeaders,
} from '../types.js';
import { createAxiosInstance } from '../utils/axiosClient.js';
@@ -28,18 +29,34 @@ import { FailedRequestError } from '../utils/errors.js';
export class TurboHTTPService implements TurboHTTPServiceInterface {
protected axios: AxiosInstance;
+ protected logger: TurboLogger;
+
constructor({
url,
retryConfig,
+ logger,
}: {
url: string;
retryConfig?: IAxiosRetryConfig;
+ logger: TurboLogger;
}) {
+ this.logger = logger;
this.axios = createAxiosInstance({
axiosConfig: {
baseURL: url,
+ onUploadProgress: (progressEvent) => {
+ this.logger.debug(`Uploading...`, {
+ percent: Math.floor((progressEvent.progress ?? 0) * 100),
+ loaded: `${progressEvent.loaded} bytes`,
+ total: `${progressEvent.total} bytes`,
+ });
+ if (progressEvent.progress === 1) {
+ this.logger.debug(`Upload complete!`);
+ }
+ },
},
retryConfig,
+ logger: this.logger,
});
}
async get({
diff --git a/src/common/logger.ts b/src/common/logger.ts
new file mode 100644
index 00000000..ad6c07dc
--- /dev/null
+++ b/src/common/logger.ts
@@ -0,0 +1,77 @@
+/**
+ * Copyright (C) 2022-2023 Permanent Data Solutions, Inc. All Rights Reserved.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+import winston, { createLogger, format, transports } from 'winston';
+
+import { TurboLogger } from '../types.js';
+import { version } from '../version.js';
+
+export class TurboWinstonLogger implements TurboLogger {
+ protected logger: winston.Logger;
+ constructor({
+ level = 'info',
+ logFormat = 'simple',
+ }: {
+ level?: 'info' | 'debug' | 'error' | 'none' | undefined;
+ logFormat?: 'simple' | 'json' | undefined;
+ } = {}) {
+ this.logger = createLogger({
+ level,
+ defaultMeta: { client: 'turbo-sdk', version },
+ silent: level === 'none',
+ format: getLogFormat(logFormat),
+ transports: [new transports.Console()],
+ });
+ }
+
+ info(message: string, ...args: any[]) {
+ this.logger.info(message, ...args);
+ }
+
+ warn(message: string, ...args: any[]) {
+ this.logger.warn(message, ...args);
+ }
+
+ error(message: string, ...args: any[]) {
+ this.logger.error(message, ...args);
+ }
+
+ debug(message: string, ...args: any[]) {
+ this.logger.debug(message, ...args);
+ }
+
+ setLogLevel(level: string) {
+ this.logger.level = level;
+ }
+
+ setLogFormat(logFormat: string) {
+ this.logger.format = getLogFormat(logFormat);
+ }
+}
+
+function getLogFormat(logFormat: string) {
+ return format.combine(
+ format((info) => {
+ if (info.stack && info.level !== 'error') {
+ delete info.stack;
+ }
+ return info;
+ })(),
+ format.errors({ stack: true }), // Ensure errors show a stack trace
+ format.timestamp(),
+ logFormat === 'json' ? format.json() : format.simple(),
+ );
+}
diff --git a/src/common/payment.ts b/src/common/payment.ts
index 91acdfeb..4bb51623 100644
--- a/src/common/payment.ts
+++ b/src/common/payment.ts
@@ -25,6 +25,7 @@ import {
TurboCountriesResponse,
TurboCurrenciesResponse,
TurboFiatToArResponse,
+ TurboLogger,
TurboPriceResponse,
TurboRatesResponse,
TurboSignedRequestHeaders,
@@ -35,6 +36,7 @@ import {
TurboWincForFiatResponse,
} from '../types.js';
import { TurboHTTPService } from './http.js';
+import { TurboWinstonLogger } from './logger.js';
export const developmentPaymentServiceURL = 'https://payment.ardrive.dev';
export const defaultPaymentServiceURL = 'https://payment.ardrive.io';
@@ -43,14 +45,18 @@ export class TurboUnauthenticatedPaymentService
implements TurboUnauthenticatedPaymentServiceInterface
{
protected readonly httpService: TurboHTTPService;
+ protected logger: TurboLogger;
constructor({
url = defaultPaymentServiceURL,
retryConfig,
+ logger = new TurboWinstonLogger(),
}: TurboUnauthenticatedPaymentServiceConfiguration) {
+ this.logger = logger;
this.httpService = new TurboHTTPService({
url: `${url}/v1`,
retryConfig,
+ logger: this.logger,
});
}
@@ -154,8 +160,9 @@ export class TurboAuthenticatedPaymentService
url = defaultPaymentServiceURL,
retryConfig,
signer,
+ logger,
}: TurboAuthenticatedPaymentServiceConfiguration) {
- super({ url, retryConfig });
+ super({ url, retryConfig, logger });
this.signer = signer;
}
diff --git a/src/common/upload.ts b/src/common/upload.ts
index c92c1630..ee42bd78 100644
--- a/src/common/upload.ts
+++ b/src/common/upload.ts
@@ -19,6 +19,7 @@ import {
TurboAuthenticatedUploadServiceConfiguration,
TurboAuthenticatedUploadServiceInterface,
TurboFileFactory,
+ TurboLogger,
TurboSignedDataItemFactory,
TurboUnauthenticatedUploadServiceConfiguration,
TurboUnauthenticatedUploadServiceInterface,
@@ -26,6 +27,7 @@ import {
TurboWalletSigner,
} from '../types.js';
import { TurboHTTPService } from './http.js';
+import { TurboWinstonLogger } from './logger.js';
export const developmentUploadServiceURL = 'https://upload.ardrive.dev';
export const defaultUploadServiceURL = 'https://upload.ardrive.io';
@@ -34,14 +36,18 @@ export class TurboUnauthenticatedUploadService
implements TurboUnauthenticatedUploadServiceInterface
{
protected httpService: TurboHTTPService;
+ protected logger: TurboLogger;
constructor({
url = defaultUploadServiceURL,
retryConfig,
+ logger = new TurboWinstonLogger(),
}: TurboUnauthenticatedUploadServiceConfiguration) {
+ this.logger = logger;
this.httpService = new TurboHTTPService({
url: `${url}/v1`,
retryConfig,
+ logger: this.logger,
});
}
@@ -52,6 +58,7 @@ export class TurboUnauthenticatedUploadService
}: TurboSignedDataItemFactory &
TurboAbortSignal): Promise {
const fileSize = dataItemSizeFactory();
+ this.logger.debug('Uploading signed data item...');
// TODO: add p-limit constraint or replace with separate upload class
return this.httpService.post({
endpoint: `/tx`,
@@ -76,8 +83,9 @@ export class TurboAuthenticatedUploadService
url = defaultUploadServiceURL,
retryConfig,
signer,
+ logger,
}: TurboAuthenticatedUploadServiceConfiguration) {
- super({ url, retryConfig });
+ super({ url, retryConfig, logger });
this.signer = signer;
}
@@ -94,6 +102,7 @@ export class TurboAuthenticatedUploadService
});
const signedDataItem = dataItemStreamFactory();
const fileSize = dataItemSizeFactory();
+ this.logger.debug('Uploading signed data item...');
// TODO: add p-limit constraint or replace with separate upload class
return this.httpService.post({
endpoint: `/tx`,
diff --git a/src/node/factory.ts b/src/node/factory.ts
index aad6dd14..ef6e7c1a 100644
--- a/src/node/factory.ts
+++ b/src/node/factory.ts
@@ -29,14 +29,19 @@ export class TurboFactory extends TurboBaseFactory {
paymentServiceConfig = {},
uploadServiceConfig = {},
}: TurboAuthenticatedConfiguration) {
- const signer = new TurboNodeArweaveSigner({ privateKey });
+ const signer = new TurboNodeArweaveSigner({
+ privateKey,
+ logger: this.logger,
+ });
const paymentService = new TurboAuthenticatedPaymentService({
...paymentServiceConfig,
signer,
+ logger: this.logger,
});
const uploadService = new TurboAuthenticatedUploadService({
...uploadServiceConfig,
signer,
+ logger: this.logger,
});
return new TurboAuthenticatedClient({
uploadService,
diff --git a/src/node/signer.ts b/src/node/signer.ts
index 03c75c6b..32ec078c 100644
--- a/src/node/signer.ts
+++ b/src/node/signer.ts
@@ -20,16 +20,23 @@ import { randomBytes } from 'node:crypto';
import { Readable } from 'node:stream';
import { JWKInterface } from '../common/jwk.js';
-import { StreamSizeFactory, TurboWalletSigner } from '../types.js';
+import { StreamSizeFactory, TurboLogger, TurboWalletSigner } from '../types.js';
import { toB64Url } from '../utils/base64.js';
export class TurboNodeArweaveSigner implements TurboWalletSigner {
protected privateKey: JWKInterface;
protected signer: ArweaveSigner; // TODO: replace with internal signer class
-
+ protected logger: TurboLogger;
// TODO: replace with internal signer class
- constructor({ privateKey }: { privateKey: JWKInterface }) {
+ constructor({
+ privateKey,
+ logger,
+ }: {
+ privateKey: JWKInterface;
+ logger: TurboLogger;
+ }) {
this.privateKey = privateKey;
+ this.logger = logger;
this.signer = new ArweaveSigner(this.privateKey);
}
@@ -44,8 +51,11 @@ export class TurboNodeArweaveSigner implements TurboWalletSigner {
dataItemSizeFactory: StreamSizeFactory;
}> {
// TODO: replace with our own signer implementation
+ this.logger.debug('Signing data item...');
const [stream1, stream2] = [fileStreamFactory(), fileStreamFactory()];
const signedDataItem = await streamSigner(stream1, stream2, this.signer);
+ this.logger.debug('Successfully signed data item...');
+
// TODO: support target, anchor, and tags
const signedDataItemSize = this.calculateSignedDataHeadersSize({
dataSize: fileSizeFactory(),
diff --git a/src/types.ts b/src/types.ts
index 0d85aa01..f43796dc 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -17,7 +17,6 @@
import { IAxiosRetryConfig } from 'axios-retry';
import { Readable } from 'node:stream';
import { ReadableStream } from 'node:stream/web';
-import winston from 'winston';
import { CurrencyMap } from './common/currency.js';
import { JWKInterface } from './common/jwk.js';
@@ -121,7 +120,7 @@ type TurboAuthConfiguration = {
type TurboServiceConfiguration = {
url?: string;
retryConfig?: IAxiosRetryConfig;
- logger?: winston.Logger;
+ logger?: TurboLogger;
};
export type TurboUnauthenticatedUploadServiceConfiguration =
@@ -139,6 +138,15 @@ export type TurboUnauthenticatedConfiguration = {
uploadServiceConfig?: TurboUnauthenticatedUploadServiceConfiguration;
};
+export interface TurboLogger {
+ setLogLevel: (level: string) => void;
+ setLogFormat: (logFormat: string) => void;
+ info: (message: string, ...args: any[]) => void;
+ warn: (message: string, ...args: any[]) => void;
+ error: (message: string, ...args: any[]) => void;
+ debug: (message: string, ...args: any[]) => void;
+}
+
export type TurboAuthenticatedConfiguration =
TurboUnauthenticatedConfiguration & {
privateKey: TurboWallet;
diff --git a/src/utils/axiosClient.ts b/src/utils/axiosClient.ts
index 1ffa6be2..a610789e 100644
--- a/src/utils/axiosClient.ts
+++ b/src/utils/axiosClient.ts
@@ -17,6 +17,8 @@
import axios, { AxiosInstance, AxiosRequestConfig, CanceledError } from 'axios';
import axiosRetry, { IAxiosRetryConfig } from 'axios-retry';
+import { TurboWinstonLogger } from '../common/logger.js';
+import { TurboLogger } from '../types.js';
import { version } from '../version.js';
export const defaultRequestHeaders = {
@@ -27,9 +29,11 @@ export const defaultRequestHeaders = {
export interface AxiosInstanceParameters {
axiosConfig?: Omit;
retryConfig?: IAxiosRetryConfig;
+ logger?: TurboLogger;
}
export const createAxiosInstance = ({
+ logger = new TurboWinstonLogger(),
axiosConfig = {},
retryConfig = {
retryDelay: axiosRetry.exponentialDelay,
@@ -41,15 +45,16 @@ export const createAxiosInstance = ({
);
},
onRetry: (retryCount, error) => {
- console.debug(
- `Request failed, ${error}. Retry attempt #${retryCount}...`,
- );
+ logger.debug(`Request failed, ${error}. Retry attempt #${retryCount}...`);
},
},
-}: AxiosInstanceParameters): AxiosInstance => {
+}: AxiosInstanceParameters = {}): AxiosInstance => {
const axiosInstance = axios.create({
...axiosConfig,
- headers: defaultRequestHeaders,
+ headers: {
+ ...axiosConfig.headers,
+ ...defaultRequestHeaders,
+ },
validateStatus: () => true, // don't throw on non-200 status codes
});
diff --git a/src/web/factory.ts b/src/web/factory.ts
index 6dbd3a16..f199b8d7 100644
--- a/src/web/factory.ts
+++ b/src/web/factory.ts
@@ -29,14 +29,19 @@ export class TurboFactory extends TurboBaseFactory {
paymentServiceConfig = {},
uploadServiceConfig = {},
}: TurboAuthenticatedConfiguration) {
- const signer = new TurboWebArweaveSigner({ privateKey });
+ const signer = new TurboWebArweaveSigner({
+ privateKey,
+ logger: this.logger,
+ });
const paymentService = new TurboAuthenticatedPaymentService({
...paymentServiceConfig,
signer,
+ logger: this.logger,
});
const uploadService = new TurboAuthenticatedUploadService({
...uploadServiceConfig,
signer,
+ logger: this.logger,
});
return new TurboAuthenticatedClient({
uploadService,
diff --git a/src/web/signer.ts b/src/web/signer.ts
index f46b1f5d..5529a83f 100644
--- a/src/web/signer.ts
+++ b/src/web/signer.ts
@@ -20,16 +20,23 @@ import { randomBytes } from 'node:crypto';
import { ReadableStream } from 'node:stream/web';
import { JWKInterface } from '../common/jwk.js';
-import { StreamSizeFactory, TurboWalletSigner } from '../types.js';
+import { StreamSizeFactory, TurboLogger, TurboWalletSigner } from '../types.js';
import { toB64Url } from '../utils/base64.js';
import { readableStreamToBuffer } from '../utils/readableStream.js';
export class TurboWebArweaveSigner implements TurboWalletSigner {
protected privateKey: JWKInterface;
protected signer: ArweaveSigner; // TODO: replace with internal signer class
-
- constructor({ privateKey }: { privateKey: JWKInterface }) {
+ protected logger: TurboLogger;
+ constructor({
+ privateKey,
+ logger,
+ }: {
+ privateKey: JWKInterface;
+ logger: TurboLogger;
+ }) {
this.privateKey = privateKey;
+ this.logger = logger;
this.signer = new ArweaveSigner(this.privateKey);
}
@@ -49,9 +56,11 @@ export class TurboWebArweaveSigner implements TurboWalletSigner {
stream: fileStreamFactory(),
size: fileSizeFactory(),
});
+ this.logger.debug('Signing data item...');
// TODO: support target, anchor and tags for upload
const signedDataItem = createData(buffer, this.signer, {});
await signedDataItem.sign(this.signer);
+ this.logger.debug('Successfully signed data item...');
return {
// while this returns a Buffer - it needs to match our return type for uploading
dataItemStreamFactory: () => signedDataItem.getRaw(),