Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* @typedef {import('typeorm').MigrationInterface} MigrationInterface
* @typedef {import('typeorm').QueryRunner} QueryRunner
*/

/**
* Adds `fiat_output.scryptDepositNotifiedDate` for the Scrypt deposit notify sweep.
* Existing completed LiqManagement payouts to Scrypt are backfilled so the sweep does
* not re-send historical deposits; the fixed timestamp is an audit marker only
* (treated as done because the sweep starts here — not proven notified at the broker).
*
* @class
* @implements {MigrationInterface}
*/
module.exports = class AddFiatOutputScryptDepositNotifiedDate1784700000001 {
name = 'AddFiatOutputScryptDepositNotifiedDate1784700000001';

/**
* @param {QueryRunner} queryRunner
*/
async up(queryRunner) {
await queryRunner.query(`SET LOCAL lock_timeout = '5s'`);
await queryRunner.query(`ALTER TABLE "fiat_output" ADD "scryptDepositNotifiedDate" TIMESTAMP`);
// Audit marker only: treated as done because the sweep starts now — not proven notified at the broker.
await queryRunner.query(`
UPDATE "fiat_output"
SET "scryptDepositNotifiedDate" = TIMESTAMP '2026-07-23 12:00:00'
WHERE "isComplete" = true
AND "type" = 'LiqManagement'
AND "name" LIKE '%Scrypt Digital Trading%'
AND "scryptDepositNotifiedDate" IS NULL
`);
}

/**
* @param {QueryRunner} queryRunner
*/
async down(queryRunner) {
await queryRunner.query(`SET LOCAL lock_timeout = '5s'`);
await queryRunner.query(`ALTER TABLE "fiat_output" DROP COLUMN "scryptDepositNotifiedDate"`);
}
};
41 changes: 41 additions & 0 deletions migration/1784807670011-AddRealUnitWalletApp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Add RealUnit as a selectable wallet_app for DFX OpenCryptoPay payment-link pages.
//
// RealUnit consumes the OpenCryptoPay LNURL only as the payment-request identifier and
// settles exclusively on-chain in ZCHF on Ethereum. blockchains='Ethereum' + assets resolved
// at migration-run-time by uniqueName 'Ethereum/ZCHF' yield supportedMethods=['Ethereum'] /
// supportedAssets=[Ethereum/ZCHF], so it qualifies for the Ethereum/ZCHF transfer option of an
// OCP payment (frontend matches on supportedAsset.name === 'ZCHF'). deepLink stays the bare
// custom scheme 'realunit-wallet:'.

/**
* @typedef {import('typeorm').MigrationInterface} MigrationInterface
* @typedef {import('typeorm').QueryRunner} QueryRunner
*/

module.exports = class AddRealUnitWalletApp1784807670011 {
name = 'AddRealUnitWalletApp1784807670011';

async up(queryRunner) {
// Idempotent guard against UNIQUE(name): skip if RealUnit already exists.
const existing = (await queryRunner.query(`SELECT "id" FROM "wallet_app" WHERE "name" = 'RealUnit'`)).at(0);
if (existing) return;

// Asset ids are env-specific SERIAL values; resolve the stable uniqueName at run-time.
const ethZchfAsset = (await queryRunner.query(`SELECT "id" FROM "asset" WHERE "uniqueName" = 'Ethereum/ZCHF'`)).at(
0,
);
if (!ethZchfAsset) {
throw new Error(
"AddRealUnitWalletApp: asset with uniqueName 'Ethereum/ZCHF' not found - cannot wire wallet_app.assets",
);
}

await queryRunner.query(
`INSERT INTO "wallet_app" ("name", "websiteUrl", "iconUrl", "deepLink", "hasActionDeepLink", "appStoreUrl", "playStoreUrl", "recommended", "blockchains", "assets", "semiCompatible", "active") VALUES ('RealUnit', 'https://realunit.app', 'https://dfx.swiss/images/app/realunit.webp', 'realunit-wallet:', NULL, 'https://apps.apple.com/ch/app/realunit/id6759720010', 'https://play.google.com/store/apps/details?id=swiss.realunit.app', false, 'Ethereum', '${ethZchfAsset.id}', NULL, true)`,
);
}

async down(queryRunner) {
await queryRunner.query(`DELETE FROM "wallet_app" WHERE "name" = 'RealUnit'`);
}
};
7 changes: 7 additions & 0 deletions src/integration/exchange/dto/scrypt.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ export interface ScryptWithdrawStatus {
rejectText?: string;
}

export interface ScryptDepositStatus {
id: string;
status: ScryptTransactionStatus;
rejectReason?: string;
rejectText?: string;
}

// --- TRADE TYPES --- //

export enum ScryptTradeSide {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { ScryptOrderStatus, ScryptTransactionStatus } from '../../dto/scrypt.dto';
import {
ScryptBalanceTransaction,
ScryptOrderStatus,
ScryptTransactionStatus,
ScryptTransactionType,
} from '../../dto/scrypt.dto';
import { ScryptMessageType, ScryptWebSocketConnection } from '../scrypt-websocket-connection';
import { ScryptService } from '../scrypt.service';

Expand Down Expand Up @@ -43,6 +48,7 @@ describe('ScryptService', () => {
fetchAll: jest.Mock;
onReconnect: jest.Mock;
subscribeToStream: jest.Mock;
send: jest.Mock;
};

beforeEach(async () => {
Expand Down Expand Up @@ -456,4 +462,88 @@ describe('ScryptService', () => {
expect(executionReportCalls).toHaveLength(2);
expect(balanceTransactionCalls).toHaveLength(2);
});

describe('getDepositStatus', () => {
it('returns null on cache miss', () => {
expect(service.getDepositStatus('missing-req-id')).toBeNull();
});

it('returns null when the cached transaction is a withdrawal', () => {
const clReqId = 'withdraw-req';
(service as any).balanceTransactions.set(clReqId, {
TransactionID: 'tx-w-1',
ClReqID: clReqId,
Currency: 'CHF',
TransactionType: ScryptTransactionType.WITHDRAWAL,
Status: ScryptTransactionStatus.COMPLETED,
Quantity: '100',
} satisfies ScryptBalanceTransaction);

expect(service.getDepositStatus(clReqId)).toBeNull();
});

it('returns the mapped deposit status for a cached deposit transaction', () => {
const clReqId = 'deposit-req';
(service as any).balanceTransactions.set(clReqId, {
TransactionID: 'tx-d-1',
ClReqID: clReqId,
Currency: 'CHF',
TransactionType: ScryptTransactionType.DEPOSIT,
Status: ScryptTransactionStatus.COMPLETED,
Quantity: '250.5',
RejectReason: 'reason-code',
RejectText: 'human readable',
} satisfies ScryptBalanceTransaction);

expect(service.getDepositStatus(clReqId)).toEqual({
id: 'tx-d-1',
status: ScryptTransactionStatus.COMPLETED,
rejectReason: 'reason-code',
rejectText: 'human readable',
});
});
});

describe('sendDepositRequest', () => {
it('sends a NewDepositRequest with TxHashes derived from reqId when txHashes is omitted', async () => {
const timeStamp = new Date('2026-07-23T12:00:00.000Z');
await service.sendDepositRequest({
currency: 'CHF',
amount: 123.45,
reqId: 'DEPOSIT-99',
timeStamp,
});

expect(instance.send).toHaveBeenCalledWith(ScryptMessageType.NEW_DEPOSIT_REQUEST, [
{
Currency: 'CHF',
ClReqID: 'DEPOSIT-99',
Quantity: '123.45',
TransactTime: '2026-07-23T12:00:00.000Z',
TxHashes: [{ TxHash: 'DEPOSIT-99' }],
},
]);
});

it('sends a NewDepositRequest with the provided txHashes array', async () => {
const timeStamp = new Date('2026-07-23T13:30:00.000Z');
await service.sendDepositRequest({
currency: 'EUR',
amount: 50,
reqId: 'E2E-1',
timeStamp,
txHashes: ['0xabc', '0xdef'],
});

expect(instance.send).toHaveBeenCalledWith(ScryptMessageType.NEW_DEPOSIT_REQUEST, [
{
Currency: 'EUR',
ClReqID: 'E2E-1',
Quantity: '50',
TransactTime: '2026-07-23T13:30:00.000Z',
TxHashes: [{ TxHash: '0xabc' }, { TxHash: '0xdef' }],
},
]);
});
});
});
14 changes: 14 additions & 0 deletions src/integration/exchange/services/scrypt.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { PricingProvider } from 'src/subdomains/supporting/pricing/services/inte
import {
ScryptBalance,
ScryptBalanceTransaction,
ScryptDepositStatus,
ScryptExecutionReport,
ScryptMarketDataSnapshot,
ScryptOrderBook,
Expand Down Expand Up @@ -268,6 +269,19 @@ export class ScryptService extends PricingProvider {

// --- DEPOSITS --- //

getDepositStatus(clReqId: string): ScryptDepositStatus | null {
const transaction = this.balanceTransactions.get(clReqId);

if (!transaction || transaction.TransactionType !== ScryptTransactionType.DEPOSIT) return null;

return {
id: transaction.TransactionID,
status: transaction.Status,
rejectReason: transaction.RejectReason,
rejectText: transaction.RejectText,
};
}

async sendDepositRequest(params: {
currency: string;
amount: number;
Expand Down
1 change: 1 addition & 0 deletions src/shared/services/process.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export enum Process {
FIAT_OUTPUT_BATCH_ID_UPDATE_JOB = 'FiatOutputBatchIdUpdateJob',
FIAT_OUTPUT_TRANSMISSION_CHECK = 'FiatOutputTransmissionCheck',
FIAT_OUTPUT_BANK_TX_SEARCH = 'FiatOutputBankTxSearch',
FIAT_OUTPUT_SCRYPT_DEPOSIT_NOTIFY = 'FiatOutputScryptDepositNotify',
FIAT_OUTPUT_YAPEAL_TRANSMISSION = 'FiatOutputYapealTransmission',
FIAT_OUTPUT_YAPEAL_STATUS_CHECK = 'FiatOutputYapealStatusCheck',
FIAT_OUTPUT_OLKYPAY_TRANSMISSION = 'FiatOutputOlkypayTransmission',
Expand Down
Loading
Loading