forked from MergeFi/backend
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathescrow.entity.ts
More file actions
144 lines (121 loc) · 4.8 KB
/
Copy pathescrow.entity.ts
File metadata and controls
144 lines (121 loc) · 4.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
import {
Check,
Column,
CreateDateColumn,
Entity,
JoinColumn,
OneToMany,
OneToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { Bounty } from './bounty.entity';
import { Milestone } from './milestone.entity';
import { MaintenancePool } from './maintenance-pool.entity';
import { Payment } from './payment.entity';
import { AssetType, EscrowStatus } from '../enums';
/**
* Local ledger of an on-chain Soroban escrow contract invocation. One row per
* "escrow instance" — a bounty, a milestone pool, or a maintenance pool.
* The actual funds custody lives in the deployed escrow contract on Stellar;
* this table mirrors state so the API can serve fast reads and so we have an
* audit trail independent of Horizon/RPC availability.
*
* The parent link (bounty/milestone/maintenancePool) is `onDelete: 'SET
* NULL'`, not CASCADE: deleting a bounty/milestone must never delete the
* escrow row (and, transitively, its payments) out from under real,
* possibly still-LOCKED, funds. See #27 — an escrow whose parent was
* deleted stays a first-class, still-queryable ledger row, orphaned but
* intact, attributable to its sponsor via the denormalized `sponsorId`
* below (which survives independently of the parent row).
*
* The CHECK constraint below only enforces *at most one* parent, not
* *exactly* one: `ON DELETE SET NULL` nulls out an escrow's only parent
* column, which is precisely the state an orphaned-by-deletion escrow ends
* up in (0 of 3 set) — a CHECK requiring exactly one would make that very
* SET NULL fail with a constraint violation the moment it fires. "Exactly
* one at creation" is enforced instead where it belongs: application-side,
* in EscrowService.fund (see assertExactlyOneParent).
*/
@Entity('escrows')
@Check(
'CHK_escrow_at_most_one_parent',
`(
(CASE WHEN "bountyId" IS NOT NULL THEN 1 ELSE 0 END) +
(CASE WHEN "milestoneId" IS NOT NULL THEN 1 ELSE 0 END) +
(CASE WHEN "maintenancePoolId" IS NOT NULL THEN 1 ELSE 0 END)
) <= 1`,
)
export class Escrow {
@PrimaryGeneratedColumn('uuid')
id: string;
@OneToOne(() => Bounty, (bounty) => bounty.escrow, {
nullable: true,
onDelete: 'SET NULL',
})
@JoinColumn()
bounty: Bounty | null;
@Column({ type: 'varchar', nullable: true })
bountyId: string | null;
@OneToOne(() => Milestone, (milestone) => milestone.escrow, {
nullable: true,
onDelete: 'SET NULL',
})
@JoinColumn()
milestone: Milestone | null;
@Column({ type: 'varchar', nullable: true })
milestoneId: string | null;
@OneToOne(() => MaintenancePool, (pool) => pool.escrow, {
nullable: true,
onDelete: 'SET NULL',
})
@JoinColumn()
maintenancePool: MaintenancePool | null;
@Column({ type: 'varchar', nullable: true })
maintenancePoolId: string | null;
/**
* Denormalized sponsor identity, captured from the parent bounty/milestone
* at fund time. Sponsor-dashboard aggregates (src/sponsors/sponsors.service.ts)
* read this column directly rather than joining through bounty/milestone,
* so a locked or spent escrow is still correctly attributed to its sponsor
* even after the parent record is deleted (#27). Null for
* maintenance-pool escrows, which aren't sponsor-attributed the same way.
*/
@Column({ type: 'varchar', nullable: true })
sponsorId: string | null;
/** Deployed Soroban contract ID this escrow instance is held by. */
@Column({ type: 'varchar', nullable: true })
contractId: string | null;
@Column({ type: 'decimal', precision: 20, scale: 7 })
amount: string;
@Column({ type: 'enum', enum: AssetType, default: AssetType.USDC })
asset: AssetType;
@Column({ type: 'enum', enum: EscrowStatus, default: EscrowStatus.PENDING })
status: EscrowStatus;
@Column({ type: 'varchar', nullable: true })
fundedByAddress: string | null;
/** Stellar transaction hash of the fund/lock invocation. */
@Column({ type: 'varchar', nullable: true })
fundTxHash: string | null;
/** Stellar transaction hash of the release invocation (final release event). */
@Column({ type: 'varchar', nullable: true })
releaseTxHash: string | null;
/** Stellar transaction hash of the refund invocation, if refunded. */
@Column({ type: 'varchar', nullable: true })
refundTxHash: string | null;
/** Arbitrary metadata returned from the Soroban RPC call (sim results, ledger, etc). */
@Column({ type: 'jsonb', nullable: true })
metadata: Record<string, unknown> | null;
@OneToMany(() => Payment, (payment) => payment.escrow)
payments: Payment[];
@Column({ type: 'timestamptz', nullable: true })
lockedAt: Date | null;
@Column({ type: 'timestamptz', nullable: true })
releasedAt: Date | null;
@Column({ type: 'timestamptz', nullable: true })
refundedAt: Date | null;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}