@@ -1120,6 +1120,7 @@ export async function disputeBounty(
11201120 id : string ,
11211121 contributor : string ,
11221122 reason : string ,
1123+ evidence ?: string ,
11231124) : Promise < BountyRecord > {
11241125 return withStoreLock ( async ( ) => {
11251126 const records = listBounties ( ) ;
@@ -1145,7 +1146,10 @@ export async function disputeBounty(
11451146 type : "disputed" ,
11461147 timestamp : now ,
11471148 actor : contributor ,
1148- details : { reason } ,
1149+ details : {
1150+ reason,
1151+ ...( evidence ?. trim ( ) ? { evidence : evidence . trim ( ) } : { } ) ,
1152+ } ,
11491153 } ,
11501154 ] ,
11511155 } ;
@@ -1160,6 +1164,7 @@ export async function disputeBounty(
11601164 actor : contributor ,
11611165 metadata : {
11621166 reason,
1167+ ...( evidence ?. trim ( ) ? { evidence : evidence . trim ( ) } : { } ) ,
11631168 } ,
11641169 } ,
11651170 ] ) ;
@@ -1475,6 +1480,283 @@ export function getBountyEvents(bountyId: string): BountyEvent[] {
14751480 return bounty . events || [ ] ;
14761481}
14771482
1483+ /**
1484+ * A recorded dispute history entry for a bounty, sourced from audit logs and contract events.
1485+ */
1486+ export interface BountyDisputeRecord {
1487+ /** Unique audit record ID or dispute event ID. */
1488+ id ?: string ;
1489+ /** Bounty ID that this dispute pertains to. */
1490+ bountyId : string ;
1491+ /** Reason provided when raising the dispute. */
1492+ reason : string ;
1493+ /** Raw evidence link, CID, or text. */
1494+ evidence ?: string | null ;
1495+ /** URL link to the evidence, if applicable. */
1496+ evidenceLink ?: string | null ;
1497+ /** IPFS CID of the evidence, if applicable. */
1498+ evidenceCid ?: string | null ;
1499+ /** Unix timestamp in seconds when the dispute was created. */
1500+ timestamp : number ;
1501+ /** Unix timestamp in seconds when the dispute was created. */
1502+ disputedAt : number ;
1503+ /** Unix timestamp in seconds when the dispute was resolved (null if unresolved). */
1504+ resolvedAt ?: number | null ;
1505+ /** Dispute lifecycle timestamps. */
1506+ timestamps : {
1507+ disputedAt : number ;
1508+ resolvedAt ?: number | null ;
1509+ } ;
1510+ /** Resolution outcome ('released', 'refunded', or null if still disputed). */
1511+ resolutionOutcome ?: string | null ;
1512+ /** Alias for resolutionOutcome. */
1513+ resolution ?: string | null ;
1514+ /** Alias for resolutionOutcome. */
1515+ outcome ?: string | null ;
1516+ /** Current status of the dispute ('disputed' or 'resolved'). */
1517+ status : string ;
1518+ /** Stellar address of the actor who raised the dispute. */
1519+ actor : string ;
1520+ /** Stellar address of the user who raised the dispute. */
1521+ raisedBy : string ;
1522+ /** Stellar address of the arbiter who resolved the dispute (null if unresolved). */
1523+ resolvedBy ?: string | null ;
1524+ /** Alias for resolvedBy. */
1525+ arbiter ?: string | null ;
1526+ /** Transaction hash of the resolution payout, if resolved. */
1527+ transactionHash ?: string | null ;
1528+ }
1529+
1530+ /**
1531+ * Retrieves the dispute history and evidence for a bounty.
1532+ * Sourced from the audit log and contract events.
1533+ *
1534+ * @param {string } bountyId - The unique ID of the bounty.
1535+ * @returns {BountyDisputeRecord[] } An array of dispute entries in chronological order (empty if never disputed).
1536+ */
1537+ export function getBountyDisputes ( bountyId : string ) : BountyDisputeRecord [ ] {
1538+ const auditLogs = readAuditStore ( )
1539+ . filter ( ( log ) => log . bountyId === bountyId )
1540+ . sort ( ( a , b ) => a . timestamp - b . timestamp ) ;
1541+
1542+ const records = listBounties ( ) ;
1543+ const bounty = records . find ( ( b ) => b . id === bountyId ) ;
1544+
1545+ const disputeLogs = auditLogs . filter ( ( log ) => log . transition === "dispute" ) ;
1546+ const resolveLogs = auditLogs . filter ( ( log ) => log . transition === "resolve_dispute" ) ;
1547+
1548+ if ( disputeLogs . length > 0 ) {
1549+ return disputeLogs . map ( ( disputeLog , index ) => {
1550+ const nextDisputeLog = disputeLogs [ index + 1 ] ;
1551+ const resolveLog = resolveLogs . find (
1552+ ( r ) =>
1553+ r . timestamp >= disputeLog . timestamp &&
1554+ ( ! nextDisputeLog || r . timestamp <= nextDisputeLog . timestamp ) ,
1555+ ) ;
1556+
1557+ const metadata = disputeLog . metadata || { } ;
1558+ const reason =
1559+ ( typeof metadata . reason === "string" ? metadata . reason : undefined ) ||
1560+ bounty ?. disputeReason ||
1561+ "" ;
1562+
1563+ const rawEvidence =
1564+ ( typeof metadata . evidence === "string" ? metadata . evidence : undefined ) ||
1565+ ( typeof metadata . evidenceLink === "string" ? metadata . evidenceLink : undefined ) ||
1566+ ( typeof metadata . evidenceCid === "string" ? metadata . evidenceCid : undefined ) ||
1567+ ( typeof metadata . cid === "string" ? metadata . cid : undefined ) ||
1568+ null ;
1569+
1570+ let evidenceLink : string | null = null ;
1571+ let evidenceCid : string | null = null ;
1572+ if ( rawEvidence ) {
1573+ if ( rawEvidence . startsWith ( "http://" ) || rawEvidence . startsWith ( "https://" ) ) {
1574+ evidenceLink = rawEvidence ;
1575+ } else if ( rawEvidence . startsWith ( "ipfs://" ) ) {
1576+ evidenceLink = rawEvidence ;
1577+ evidenceCid = rawEvidence . replace ( "ipfs://" , "" ) ;
1578+ } else if ( rawEvidence . startsWith ( "Qm" ) || rawEvidence . startsWith ( "bafy" ) ) {
1579+ evidenceCid = rawEvidence ;
1580+ evidenceLink = `ipfs://${ rawEvidence } ` ;
1581+ } else {
1582+ evidenceLink = rawEvidence ;
1583+ }
1584+ }
1585+
1586+ const disputedAt = disputeLog . timestamp ;
1587+ const resolvedAt = resolveLog ? resolveLog . timestamp : null ;
1588+
1589+ let resolutionOutcome : string | null = null ;
1590+ if ( resolveLog ) {
1591+ if ( typeof resolveLog . metadata ?. resolution === "string" ) {
1592+ resolutionOutcome = resolveLog . metadata . resolution ;
1593+ } else if ( resolveLog . toStatus === "released" || resolveLog . toStatus === "refunded" ) {
1594+ resolutionOutcome = resolveLog . toStatus ;
1595+ } else if ( resolveLog . metadata ?. release === true ) {
1596+ resolutionOutcome = "released" ;
1597+ } else if ( resolveLog . metadata ?. release === false ) {
1598+ resolutionOutcome = "refunded" ;
1599+ }
1600+ }
1601+
1602+ const arbiter = resolveLog ? resolveLog . actor : null ;
1603+ const txHash =
1604+ ( typeof resolveLog ?. metadata ?. transactionHash === "string"
1605+ ? resolveLog . metadata . transactionHash
1606+ : undefined ) || null ;
1607+
1608+ return {
1609+ id : disputeLog . id ,
1610+ bountyId,
1611+ reason,
1612+ evidence : rawEvidence ,
1613+ evidenceLink,
1614+ evidenceCid,
1615+ timestamp : disputedAt ,
1616+ disputedAt,
1617+ resolvedAt,
1618+ timestamps : {
1619+ disputedAt,
1620+ resolvedAt,
1621+ } ,
1622+ resolutionOutcome,
1623+ resolution : resolutionOutcome ,
1624+ outcome : resolutionOutcome ,
1625+ status : resolutionOutcome ? "resolved" : "disputed" ,
1626+ actor : disputeLog . actor ,
1627+ raisedBy : disputeLog . actor ,
1628+ resolvedBy : arbiter ,
1629+ arbiter,
1630+ transactionHash : txHash ,
1631+ } ;
1632+ } ) ;
1633+ }
1634+
1635+ // Fallback: check bounty events if audit log is empty but events exist
1636+ if ( bounty && bounty . events ) {
1637+ const disputeEvents = bounty . events . filter ( ( e ) => e . type === "disputed" ) ;
1638+ if ( disputeEvents . length > 0 ) {
1639+ return disputeEvents . map ( ( evt , index ) => {
1640+ const nextDisputeEvt = disputeEvents [ index + 1 ] ;
1641+ const resolveEvt = bounty . events . find (
1642+ ( e ) =>
1643+ ( e . type === "released" || e . type === "refunded" ) &&
1644+ e . timestamp >= evt . timestamp &&
1645+ ( ! nextDisputeEvt || e . timestamp <= nextDisputeEvt . timestamp ) ,
1646+ ) ;
1647+
1648+ const details = evt . details || { } ;
1649+ const reason =
1650+ ( typeof details . reason === "string" ? details . reason : undefined ) ||
1651+ bounty . disputeReason ||
1652+ "" ;
1653+
1654+ const rawEvidence =
1655+ ( typeof details . evidence === "string" ? details . evidence : undefined ) ||
1656+ ( typeof details . evidenceLink === "string" ? details . evidenceLink : undefined ) ||
1657+ ( typeof details . evidenceCid === "string" ? details . evidenceCid : undefined ) ||
1658+ null ;
1659+
1660+ let evidenceLink : string | null = null ;
1661+ let evidenceCid : string | null = null ;
1662+ if ( rawEvidence ) {
1663+ if ( rawEvidence . startsWith ( "http://" ) || rawEvidence . startsWith ( "https://" ) ) {
1664+ evidenceLink = rawEvidence ;
1665+ } else if ( rawEvidence . startsWith ( "ipfs://" ) ) {
1666+ evidenceLink = rawEvidence ;
1667+ evidenceCid = rawEvidence . replace ( "ipfs://" , "" ) ;
1668+ } else if ( rawEvidence . startsWith ( "Qm" ) || rawEvidence . startsWith ( "bafy" ) ) {
1669+ evidenceCid = rawEvidence ;
1670+ evidenceLink = `ipfs://${ rawEvidence } ` ;
1671+ } else {
1672+ evidenceLink = rawEvidence ;
1673+ }
1674+ }
1675+
1676+ const disputedAt = evt . timestamp ;
1677+ const resolvedAt = resolveEvt ? resolveEvt . timestamp : null ;
1678+
1679+ let resolutionOutcome : string | null = null ;
1680+ if ( resolveEvt ) {
1681+ if ( typeof resolveEvt . details ?. resolution === "string" ) {
1682+ resolutionOutcome = resolveEvt . details . resolution ;
1683+ } else {
1684+ resolutionOutcome = resolveEvt . type === "released" ? "released" : "refunded" ;
1685+ }
1686+ }
1687+
1688+ const arbiter = resolveEvt ? resolveEvt . actor || null : null ;
1689+ const txHash =
1690+ ( typeof resolveEvt ?. details ?. transactionHash === "string"
1691+ ? ( resolveEvt . details . transactionHash as string )
1692+ : undefined ) || null ;
1693+
1694+ return {
1695+ id : `EVT-${ disputedAt } ` ,
1696+ bountyId,
1697+ reason,
1698+ evidence : rawEvidence ,
1699+ evidenceLink,
1700+ evidenceCid,
1701+ timestamp : disputedAt ,
1702+ disputedAt,
1703+ resolvedAt,
1704+ timestamps : {
1705+ disputedAt,
1706+ resolvedAt,
1707+ } ,
1708+ resolutionOutcome,
1709+ resolution : resolutionOutcome ,
1710+ outcome : resolutionOutcome ,
1711+ status : resolutionOutcome ? "resolved" : "disputed" ,
1712+ actor : evt . actor || bounty . contributor || "" ,
1713+ raisedBy : evt . actor || bounty . contributor || "" ,
1714+ resolvedBy : arbiter ,
1715+ arbiter,
1716+ transactionHash : txHash ,
1717+ } ;
1718+ } ) ;
1719+ }
1720+
1721+ if ( bounty . status === "disputed" || ( bounty . disputedAt && bounty . disputedAt > 0 ) ) {
1722+ const disputedAt = bounty . disputedAt || bounty . createdAt ;
1723+ const isResolved = bounty . status === "released" || bounty . status === "refunded" ;
1724+ const resolutionOutcome = isResolved ? bounty . status : null ;
1725+ const resolvedAt = isResolved ? ( bounty . releasedAt || bounty . refundedAt || null ) : null ;
1726+ const txHash = isResolved ? ( bounty . releasedTxHash || bounty . refundedTxHash || null ) : null ;
1727+
1728+ return [
1729+ {
1730+ id : `DISP-${ disputedAt } ` ,
1731+ bountyId,
1732+ reason : bounty . disputeReason || "" ,
1733+ evidence : null ,
1734+ evidenceLink : null ,
1735+ evidenceCid : null ,
1736+ timestamp : disputedAt ,
1737+ disputedAt,
1738+ resolvedAt,
1739+ timestamps : {
1740+ disputedAt,
1741+ resolvedAt,
1742+ } ,
1743+ resolutionOutcome,
1744+ resolution : resolutionOutcome ,
1745+ outcome : resolutionOutcome ,
1746+ status : resolutionOutcome ? "resolved" : "disputed" ,
1747+ actor : bounty . contributor || "" ,
1748+ raisedBy : bounty . contributor || "" ,
1749+ resolvedBy : null ,
1750+ arbiter : null ,
1751+ transactionHash : txHash ,
1752+ } ,
1753+ ] ;
1754+ }
1755+ }
1756+
1757+ return [ ] ;
1758+ }
1759+
14781760/**
14791761 * Aggregate metrics and performance tracking data for a maintainer.
14801762 */
0 commit comments