소스 검색

ts: add error log parsing to ts client (#1640)

Paul 3 년 전
부모
커밋
9afdb17ac2

+ 1 - 0
CHANGELOG.md

@@ -23,6 +23,7 @@ incremented for features.
 * lang: Handle arrays with const as size in instruction data ([#1623](https://github.com/project-serum/anchor/issues/1623).
 * spl: Add support for revoke instruction ([#1493](https://github.com/project-serum/anchor/pull/1493)).
 * ts: Add provider parameter to `Spl.token` factory method ([#1597](https://github.com/project-serum/anchor/pull/1597)).
+* ts: Add `AnchorError` with program stack and also a program stack for non-`AnchorError` errors ([#1640](https://github.com/project-serum/anchor/pull/1640)). `AnchorError` is not returned for `processed` tx that have `skipPreflight` set to `true` (it falls back to `ProgramError` or the raw solana library error).
 
 ### Fixes
 

+ 27 - 14
tests/bpf-upgradeable-state/tests/bpf-upgradable-state.ts

@@ -1,5 +1,5 @@
 import * as anchor from "@project-serum/anchor";
-import { Program } from "@project-serum/anchor";
+import { AnchorError, Program } from "@project-serum/anchor";
 import { findProgramAddressSync } from "@project-serum/anchor/dist/cjs/utils/pubkey";
 import { PublicKey } from "@solana/web3.js";
 import assert from "assert";
@@ -78,9 +78,11 @@ describe("bpf_upgradeable_state", () => {
         signers: [settings, authority],
       });
       assert.ok(false);
-    } catch (err) {
-      assert.equal(err.code, 2003);
-      assert.equal(err.msg, "A raw constraint was violated");
+    } catch (_err) {
+      assert.ok(_err instanceof AnchorError);
+      const err: AnchorError = _err;
+      assert.equal(err.error.errorCode.number, 2003);
+      assert.equal(err.error.errorMessage, "A raw constraint was violated");
     }
   });
 
@@ -98,9 +100,14 @@ describe("bpf_upgradeable_state", () => {
         signers: [settings],
       });
       assert.ok(false);
-    } catch (err) {
-      assert.equal(err.code, 3013);
-      assert.equal(err.msg, "The given account is not a program data account");
+    } catch (_err) {
+      assert.ok(_err instanceof AnchorError);
+      const err: AnchorError = _err;
+      assert.equal(err.error.errorCode.number, 3013);
+      assert.equal(
+        err.error.errorMessage,
+        "The given account is not a program data account"
+      );
     }
   });
 
@@ -118,10 +125,12 @@ describe("bpf_upgradeable_state", () => {
         signers: [settings],
       });
       assert.ok(false);
-    } catch (err) {
-      assert.equal(err.code, 3007);
+    } catch (_err) {
+      assert.ok(_err instanceof AnchorError);
+      const err: AnchorError = _err;
+      assert.equal(err.error.errorCode.number, 3007);
       assert.equal(
-        err.msg,
+        err.error.errorMessage,
         "The given account is owned by a different program than expected"
       );
     }
@@ -149,8 +158,10 @@ describe("bpf_upgradeable_state", () => {
         signers: [settings],
       });
       assert.ok(false);
-    } catch (err) {
-      assert.equal(err.code, 6000);
+    } catch (_err) {
+      assert.ok(_err instanceof AnchorError);
+      const err: AnchorError = _err;
+      assert.equal(err.error.errorCode.number, 6000);
     }
   });
 
@@ -176,8 +187,10 @@ describe("bpf_upgradeable_state", () => {
         signers: [settings],
       });
       assert.ok(false);
-    } catch (err) {
-      assert.equal(err.code, 2003);
+    } catch (_err) {
+      assert.ok(_err instanceof AnchorError);
+      const err: AnchorError = _err;
+      assert.equal(err.error.errorCode.number, 2003);
     }
   });
 });

+ 1 - 1
tests/declare-id/Anchor.toml

@@ -6,4 +6,4 @@ wallet = "~/.config/solana/id.json"
 declare_id = "FJcF5c8HncdfAgjPjTH49GAEypkJCG2ZADh2xhduNi5B"
 
 [scripts]
-test = "yarn run mocha -t 1000000 tests/"
+test = "yarn run ts-mocha -t 1000000 tests/*.ts"

+ 0 - 17
tests/declare-id/tests/declare-id.js

@@ -1,17 +0,0 @@
-const anchor = require("@project-serum/anchor");
-const splToken = require("@solana/spl-token");
-const assert = require("assert");
-
-describe("declare_id", () => {
-  anchor.setProvider(anchor.Provider.local());
-  const program = anchor.workspace.DeclareId;
-
-  it("throws error!", async () => {
-    try {
-      await program.rpc.initialize();
-      assert.ok(false);
-    } catch (err) {
-      assert.equal(err.code, 4100);
-    }
-  });
-});

+ 22 - 0
tests/declare-id/tests/declare-id.ts

@@ -0,0 +1,22 @@
+import * as anchor from "@project-serum/anchor";
+import { AnchorError, Program } from "@project-serum/anchor";
+import splToken from "@solana/spl-token";
+import { DeclareId } from "../target/types/declare_id";
+
+import { assert } from "chai";
+
+describe("declare_id", () => {
+  anchor.setProvider(anchor.Provider.local());
+  const program = anchor.workspace.DeclareId as Program<DeclareId>;
+
+  it("throws error!", async () => {
+    try {
+      await program.rpc.initialize();
+      assert.ok(false);
+    } catch (_err) {
+      assert.ok(_err instanceof AnchorError);
+      const err: AnchorError = _err;
+      assert.equal(err.error.errorCode.number, 4100);
+    }
+  });
+});

+ 10 - 0
tests/declare-id/tsconfig.json

@@ -0,0 +1,10 @@
+{
+    "compilerOptions": {
+        "types": ["mocha", "chai", "node"],
+        "typeRoots": ["./node_modules/@types"],
+        "lib": ["es2015"],
+        "module": "commonjs",
+        "target": "es6",
+        "esModuleInterop": true
+    }
+}

+ 1 - 1
tests/errors/Anchor.toml

@@ -6,7 +6,7 @@ wallet = "~/.config/solana/id.json"
 errors = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"
 
 [scripts]
-test = "yarn run mocha -t 1000000 tests/"
+test = "yarn run ts-mocha -t 1000000 tests/*.ts"
 
 [features]
 seeds = false

+ 149 - 75
tests/errors/tests/errors.js → tests/errors/tests/errors.ts

@@ -1,13 +1,26 @@
-const assert = require("assert");
-const anchor = require("@project-serum/anchor");
-const { Account, Transaction, TransactionInstruction } = anchor.web3;
-const { TOKEN_PROGRAM_ID, Token } = require("@solana/spl-token");
-const { Keypair } = require("@solana/web3.js");
+import * as anchor from "@project-serum/anchor";
+import {
+  Program,
+  BN,
+  IdlAccounts,
+  AnchorError,
+  ProgramError,
+} from "@project-serum/anchor";
+import {
+  PublicKey,
+  Keypair,
+  SystemProgram,
+  Transaction,
+  TransactionInstruction,
+} from "@solana/web3.js";
+import { TOKEN_PROGRAM_ID, Token } from "@solana/spl-token";
+import { assert, expect } from "chai";
+import { Errors } from "../target/types/errors";
 
 // sleep to allow logs to come in
 const sleep = (ms) =>
   new Promise((resolve) => {
-    setTimeout(() => resolve(), ms);
+    setTimeout(() => resolve(0), ms);
   });
 
 const withLogTest = async (callback, expectedLogs) => {
@@ -45,7 +58,6 @@ const withLogTest = async (callback, expectedLogs) => {
     anchor.getProvider().connection.removeOnLogsListener(listener);
     throw err;
   }
-  await sleep(3000);
   anchor.getProvider().connection.removeOnLogsListener(listener);
   assert.ok(logTestOk);
 };
@@ -54,21 +66,33 @@ describe("errors", () => {
   // Configure the client to use the local cluster.
   const localProvider = anchor.Provider.local();
   localProvider.opts.skipPreflight = true;
+  // processed failed tx do not result in AnchorErrors in the client
+  // because we cannot get logs for them (only through overkill `onLogs`)
+  localProvider.opts.commitment = "confirmed";
   anchor.setProvider(localProvider);
 
-  const program = anchor.workspace.Errors;
+  const program = anchor.workspace.Errors as Program<Errors>;
 
   it("Emits a Hello error", async () => {
     await withLogTest(async () => {
       try {
         const tx = await program.rpc.hello();
         assert.ok(false);
-      } catch (err) {
+      } catch (_err) {
+        assert.ok(_err instanceof AnchorError);
+        const err: AnchorError = _err;
         const errMsg =
           "This is an error message clients will automatically display";
-        assert.equal(err.toString(), errMsg);
-        assert.equal(err.msg, errMsg);
-        assert.equal(err.code, 6000);
+        const fullErrMsg =
+          "AnchorError thrown in programs/errors/src/lib.rs:13. Error Code: Hello. Error Number: 6000. Error Message: This is an error message clients will automatically display.";
+        assert.equal(err.toString(), fullErrMsg);
+        assert.equal(err.error.errorMessage, errMsg);
+        assert.equal(err.error.errorCode.number, 6000);
+        assert.equal(err.program.toString(), program.programId.toString());
+        expect(err.error.origin).to.deep.equal({
+          file: "programs/errors/src/lib.rs",
+          line: 13,
+        });
       }
     }, [
       "Program log: AnchorError thrown in programs/errors/src/lib.rs:13. Error Code: Hello. Error Number: 6000. Error Message: This is an error message clients will automatically display.",
@@ -79,12 +103,14 @@ describe("errors", () => {
     try {
       const tx = await program.rpc.testRequire();
       assert.ok(false);
-    } catch (err) {
+    } catch (_err) {
+      assert.ok(_err instanceof AnchorError);
+      const err: AnchorError = _err;
       const errMsg =
         "This is an error message clients will automatically display";
-      assert.equal(err.toString(), errMsg);
-      assert.equal(err.msg, errMsg);
-      assert.equal(err.code, 6000);
+      assert.equal(err.error.errorMessage, errMsg);
+      assert.equal(err.error.errorCode.number, 6000);
+      assert.equal(err.error.errorCode.code, "Hello");
     }
   });
 
@@ -92,12 +118,13 @@ describe("errors", () => {
     try {
       const tx = await program.rpc.testErr();
       assert.ok(false);
-    } catch (err) {
+    } catch (_err) {
+      assert.ok(_err instanceof AnchorError);
+      const err: AnchorError = _err;
       const errMsg =
         "This is an error message clients will automatically display";
-      assert.equal(err.toString(), errMsg);
-      assert.equal(err.msg, errMsg);
-      assert.equal(err.code, 6000);
+      assert.equal(err.error.errorMessage, errMsg);
+      assert.equal(err.error.errorCode.number, 6000);
     }
   });
 
@@ -107,7 +134,10 @@ describe("errors", () => {
         const tx = await program.rpc.testProgramError();
         assert.ok(false);
       } catch (err) {
-        // No-op (withLogTest expects the callback to catch the initial tx error)
+        expect(err.programErrorStack.map((pk) => pk.toString())).to.deep.equal([
+          program.programId.toString(),
+        ]);
+        expect(err.program.toString()).to.equal(program.programId.toString());
       }
     }, [
       "Program log: ProgramError occurred. Error Code: InvalidAccountData. Error Number: 17179869184. Error Message: An account's data contents was invalid.",
@@ -120,7 +150,9 @@ describe("errors", () => {
         const tx = await program.rpc.testProgramErrorWithSource();
         assert.ok(false);
       } catch (err) {
-        // No-op (withLogTest expects the callback to catch the initial tx error)
+        expect(err.programErrorStack.map((pk) => pk.toString())).to.deep.equal([
+          program.programId.toString(),
+        ]);
       }
     }, [
       "Program log: ProgramError thrown in programs/errors/src/lib.rs:38. Error Code: InvalidAccountData. Error Number: 17179869184. Error Message: An account's data contents was invalid.",
@@ -131,11 +163,11 @@ describe("errors", () => {
     try {
       const tx = await program.rpc.helloNoMsg();
       assert.ok(false);
-    } catch (err) {
-      const errMsg = "HelloNoMsg";
-      assert.equal(err.toString(), errMsg);
-      assert.equal(err.msg, errMsg);
-      assert.equal(err.code, 6000 + 123);
+    } catch (_err) {
+      assert.ok(_err instanceof AnchorError);
+      const err: AnchorError = _err;
+      assert.equal(err.error.errorMessage, "HelloNoMsg");
+      assert.equal(err.error.errorCode.number, 6123);
     }
   });
 
@@ -143,11 +175,11 @@ describe("errors", () => {
     try {
       const tx = await program.rpc.helloNext();
       assert.ok(false);
-    } catch (err) {
-      const errMsg = "HelloNext";
-      assert.equal(err.toString(), errMsg);
-      assert.equal(err.msg, errMsg);
-      assert.equal(err.code, 6000 + 124);
+    } catch (_err) {
+      assert.ok(_err instanceof AnchorError);
+      const err: AnchorError = _err;
+      assert.equal(err.error.errorMessage, "HelloNext");
+      assert.equal(err.error.errorCode.number, 6124);
     }
   });
 
@@ -160,11 +192,12 @@ describe("errors", () => {
           },
         });
         assert.ok(false);
-      } catch (err) {
-        const errMsg = "A mut constraint was violated";
-        assert.equal(err.toString(), errMsg);
-        assert.equal(err.msg, errMsg);
-        assert.equal(err.code, 2000);
+      } catch (_err) {
+        assert.ok(_err instanceof AnchorError);
+        const err: AnchorError = _err;
+        assert.equal(err.error.errorMessage, "A mut constraint was violated");
+        assert.equal(err.error.errorCode.number, 2000);
+        assert.equal(err.error.origin, "my_account");
       }
     }, [
       "Program log: AnchorError caused by account: my_account. Error Code: ConstraintMut. Error Number: 2000. Error Message: A mut constraint was violated.",
@@ -187,11 +220,23 @@ describe("errors", () => {
           signers: [account],
         });
         assert.ok(false);
-      } catch (err) {
-        const errMsg = "A has_one constraint was violated";
-        assert.equal(err.toString(), errMsg);
-        assert.equal(err.msg, errMsg);
-        assert.equal(err.code, 2001);
+      } catch (_err) {
+        assert.ok(_err instanceof AnchorError);
+        const err: AnchorError = _err;
+        assert.equal(
+          err.error.errorMessage,
+          "A has one constraint was violated"
+        );
+        assert.equal(err.error.errorCode.number, 2001);
+        assert.equal(err.error.errorCode.code, "ConstraintHasOne");
+        assert.equal(err.error.origin, "my_account");
+        assert.equal(err.program.toString(), program.programId.toString());
+        expect(
+          err.error.comparedValues.map((pk) => pk.toString())
+        ).to.deep.equal([
+          "11111111111111111111111111111111",
+          "SysvarRent111111111111111111111111111111111",
+        ]);
       }
     }, [
       "Program log: AnchorError caused by account: my_account. Error Code: ConstraintHasOne. Error Number: 2001. Error Message: A has one constraint was violated.",
@@ -228,7 +273,6 @@ describe("errors", () => {
       await program.provider.send(tx);
       assert.ok(false);
     } catch (err) {
-      await sleep(3000);
       anchor.getProvider().connection.removeOnLogsListener(listener);
       const errMsg = `Error: Raw transaction ${signature} failed ({"err":{"InstructionError":[0,{"Custom":3010}]}})`;
       assert.equal(err.toString(), errMsg);
@@ -245,11 +289,12 @@ describe("errors", () => {
         },
       });
       assert.ok(false);
-    } catch (err) {
+    } catch (_err) {
+      assert.ok(_err instanceof AnchorError);
+      const err: AnchorError = _err;
       const errMsg = "HelloCustom";
-      assert.equal(err.toString(), errMsg);
-      assert.equal(err.msg, errMsg);
-      assert.equal(err.code, 6000 + 125);
+      assert.equal(err.error.errorMessage, errMsg);
+      assert.equal(err.error.errorCode.number, 6125);
     }
   });
 
@@ -264,10 +309,12 @@ describe("errors", () => {
         assert.fail(
           "Unexpected success in creating a transaction that should have fail with `AccountNotInitialized` error"
         );
-      } catch (err) {
+      } catch (_err) {
+        assert.ok(_err instanceof AnchorError);
+        const err: AnchorError = _err;
         const errMsg =
           "The program expected this account to be already initialized";
-        assert.equal(err.toString(), errMsg);
+        assert.equal(err.error.errorMessage, errMsg);
       }
     }, [
       "Program log: AnchorError caused by account: not_initialized_account. Error Code: AccountNotInitialized. Error Number: 3012. Error Message: The program expected this account to be already initialized.",
@@ -294,10 +341,12 @@ describe("errors", () => {
         assert.fail(
           "Unexpected success in creating a transaction that should have failed with `AccountOwnedByWrongProgram` error"
         );
-      } catch (err) {
+      } catch (_err) {
+        assert.ok(_err instanceof AnchorError);
+        const err: AnchorError = _err;
         const errMsg =
           "The given account is owned by a different program than expected";
-        assert.equal(err.toString(), errMsg);
+        assert.equal(err.error.errorMessage, errMsg);
       }
     }, [
       "Program log: AnchorError caused by account: wrong_account. Error Code: AccountOwnedByWrongProgram. Error Number: 3007. Error Message: The given account is owned by a different program than expected.",
@@ -315,8 +364,11 @@ describe("errors", () => {
         assert.fail(
           "Unexpected success in creating a transaction that should have failed with `ValueMismatch` error"
         );
-      } catch (err) {
-        assert.equal(err.code, 6126);
+      } catch (_err) {
+        assert.ok(_err instanceof AnchorError);
+        const err: AnchorError = _err;
+        assert.equal(err.error.errorCode.number, 6126);
+        expect(err.error.comparedValues).to.deep.equal(["5241", "124124124"]);
       }
     }, [
       "Program log: AnchorError thrown in programs/errors/src/lib.rs:68. Error Code: ValueMismatch. Error Number: 6126. Error Message: ValueMismatch.",
@@ -332,8 +384,10 @@ describe("errors", () => {
         assert.fail(
           "Unexpected success in creating a transaction that should have failed with `ValueMismatch` error"
         );
-      } catch (err) {
-        assert.equal(err.code, 2501);
+      } catch (_err) {
+        assert.ok(_err instanceof AnchorError);
+        const err: AnchorError = _err;
+        assert.equal(err.error.errorCode.number, 2501);
       }
     }, [
       "Program log: AnchorError thrown in programs/errors/src/lib.rs:73. Error Code: RequireEqViolated. Error Number: 2501. Error Message: A require_eq expression was violated.",
@@ -349,8 +403,10 @@ describe("errors", () => {
         assert.fail(
           "Unexpected success in creating a transaction that should have failed with `ValueMatch` error"
         );
-      } catch (err) {
-        assert.equal(err.code, 6127);
+      } catch (_err) {
+        assert.ok(_err instanceof AnchorError);
+        const err: AnchorError = _err;
+        assert.equal(err.error.errorCode.number, 6127);
       }
     }, [
       "Program log: AnchorError thrown in programs/errors/src/lib.rs:78. Error Code: ValueMatch. Error Number: 6127. Error Message: ValueMatch.",
@@ -366,8 +422,10 @@ describe("errors", () => {
         assert.fail(
           "Unexpected success in creating a transaction that should have failed with `RequireNeqViolated` error"
         );
-      } catch (err) {
-        assert.equal(err.code, 2503);
+      } catch (_err) {
+        assert.ok(_err instanceof AnchorError);
+        const err: AnchorError = _err;
+        assert.equal(err.error.errorCode.number, 2503);
       }
     }, [
       "Program log: AnchorError thrown in programs/errors/src/lib.rs:83. Error Code: RequireNeqViolated. Error Number: 2503. Error Message: A require_neq expression was violated.",
@@ -388,8 +446,10 @@ describe("errors", () => {
         assert.fail(
           "Unexpected success in creating a transaction that should have failed with `ValueMismatch` error"
         );
-      } catch (err) {
-        assert.equal(err.code, 6126);
+      } catch (_err) {
+        assert.ok(_err instanceof AnchorError);
+        const err: AnchorError = _err;
+        assert.equal(err.error.errorCode.number, 6126);
       }
     }, [
       "Program log: AnchorError thrown in programs/errors/src/lib.rs:88. Error Code: ValueMismatch. Error Number: 6126. Error Message: ValueMismatch.",
@@ -412,8 +472,10 @@ describe("errors", () => {
         assert.fail(
           "Unexpected success in creating a transaction that should have failed with `ValueMismatch` error"
         );
-      } catch (err) {
-        assert.equal(err.code, 2502);
+      } catch (_err) {
+        assert.ok(_err instanceof AnchorError);
+        const err: AnchorError = _err;
+        assert.equal(err.error.errorCode.number, 2502);
       }
     }, [
       "Program log: AnchorError thrown in programs/errors/src/lib.rs:97. Error Code: RequireKeysEqViolated. Error Number: 2502. Error Message: A require_keys_eq expression was violated.",
@@ -436,8 +498,10 @@ describe("errors", () => {
         assert.fail(
           "Unexpected success in creating a transaction that should have failed with `ValueMatch` error"
         );
-      } catch (err) {
-        assert.equal(err.code, 6127);
+      } catch (_err) {
+        assert.ok(_err instanceof AnchorError);
+        const err: AnchorError = _err;
+        assert.equal(err.error.errorCode.number, 6127);
       }
     }, [
       "Program log: AnchorError thrown in programs/errors/src/lib.rs:102. Error Code: ValueMatch. Error Number: 6127. Error Message: ValueMatch.",
@@ -460,8 +524,10 @@ describe("errors", () => {
         assert.fail(
           "Unexpected success in creating a transaction that should have failed with `RequireKeysNeqViolated` error"
         );
-      } catch (err) {
-        assert.equal(err.code, 2504);
+      } catch (_err) {
+        assert.ok(_err instanceof AnchorError);
+        const err: AnchorError = _err;
+        assert.equal(err.error.errorCode.number, 2504);
       }
     }, [
       "Program log: AnchorError thrown in programs/errors/src/lib.rs:111. Error Code: RequireKeysNeqViolated. Error Number: 2504. Error Message: A require_keys_neq expression was violated.",
@@ -479,8 +545,10 @@ describe("errors", () => {
         assert.fail(
           "Unexpected success in creating a transaction that should have failed with `ValueLessOrEqual` error"
         );
-      } catch (err) {
-        assert.equal(err.code, 6129);
+      } catch (_err) {
+        assert.ok(_err instanceof AnchorError);
+        const err: AnchorError = _err;
+        assert.equal(err.error.errorCode.number, 6129);
       }
     }, [
       "Program log: AnchorError thrown in programs/errors/src/lib.rs:116. Error Code: ValueLessOrEqual. Error Number: 6129. Error Message: ValueLessOrEqual.",
@@ -496,8 +564,10 @@ describe("errors", () => {
         assert.fail(
           "Unexpected success in creating a transaction that should have failed with `RequireGtViolated` error"
         );
-      } catch (err) {
-        assert.equal(err.code, 2505);
+      } catch (_err) {
+        assert.ok(_err instanceof AnchorError);
+        const err: AnchorError = _err;
+        assert.equal(err.error.errorCode.number, 2505);
       }
     }, [
       "Program log: AnchorError thrown in programs/errors/src/lib.rs:121. Error Code: RequireGtViolated. Error Number: 2505. Error Message: A require_gt expression was violated.",
@@ -513,8 +583,10 @@ describe("errors", () => {
         assert.fail(
           "Unexpected success in creating a transaction that should have failed with `ValueLess` error"
         );
-      } catch (err) {
-        assert.equal(err.code, 6128);
+      } catch (_err) {
+        assert.ok(_err instanceof AnchorError);
+        const err: AnchorError = _err;
+        assert.equal(err.error.errorCode.number, 6128);
       }
     }, [
       "Program log: AnchorError thrown in programs/errors/src/lib.rs:126. Error Code: ValueLess. Error Number: 6128. Error Message: ValueLess.",
@@ -530,8 +602,10 @@ describe("errors", () => {
         assert.fail(
           "Unexpected success in creating a transaction that should have failed with `RequireGteViolated` error"
         );
-      } catch (err) {
-        assert.equal(err.code, 2506);
+      } catch (_err) {
+        assert.ok(_err instanceof AnchorError);
+        const err: AnchorError = _err;
+        assert.equal(err.error.errorCode.number, 2506);
       }
     }, [
       "Program log: AnchorError thrown in programs/errors/src/lib.rs:131. Error Code: RequireGteViolated. Error Number: 2506. Error Message: A require_gte expression was violated.",

+ 10 - 0
tests/errors/tsconfig.json

@@ -0,0 +1,10 @@
+{
+    "compilerOptions": {
+        "types": ["mocha", "chai", "node"],
+        "typeRoots": ["./node_modules/@types"],
+        "lib": ["es2015"],
+        "module": "commonjs",
+        "target": "es6",
+        "esModuleInterop": true
+    }
+}

+ 32 - 8
tests/lockup/tests/lockup.js

@@ -3,6 +3,8 @@ const anchor = require("@project-serum/anchor");
 const serumCmn = require("@project-serum/common");
 const { TOKEN_PROGRAM_ID } = require("@solana/spl-token");
 const utils = require("./utils");
+const chai = require("chai");
+const expect = chai.expect;
 
 anchor.utils.features.set("anchor-deprecated-state");
 
@@ -118,8 +120,8 @@ describe("Lockup and Registry", () => {
         await lockup.state.rpc.whitelistAdd(e, { accounts });
       },
       (err) => {
-        assert.equal(err.code, 6008);
-        assert.equal(err.msg, "Whitelist is full");
+        assert.equal(err.error.errorCode.number, 6008);
+        assert.equal(err.error.errorMessage, "Whitelist is full");
         return true;
       }
     );
@@ -216,8 +218,11 @@ describe("Lockup and Registry", () => {
         });
       },
       (err) => {
-        assert.equal(err.code, 6007);
-        assert.equal(err.msg, "Insufficient withdrawal balance.");
+        assert.equal(err.error.errorCode.number, 6007);
+        assert.equal(
+          err.error.errorMessage,
+          "Insufficient withdrawal balance."
+        );
         return true;
       }
     );
@@ -773,8 +778,24 @@ describe("Lockup and Registry", () => {
       (err) => {
         // Solana doesn't propagate errors across CPI. So we receive the registry's error code,
         // not the lockup's.
-        const errorCode = "custom program error: 0x1784";
-        assert.ok(err.toString().split(errorCode).length === 2);
+        assert.equal(err.error.errorCode.number, 6020);
+        assert.equal(err.error.errorCode.code, "UnrealizedReward");
+        assert.equal(
+          err.error.errorMessage,
+          "Locked rewards cannot be realized until one unstaked all tokens."
+        );
+        expect(err.error.origin).to.deep.equal({
+          file: "programs/registry/src/lib.rs",
+          line: 63,
+        });
+        assert.equal(
+          err.program.toString(),
+          "HmbTLCmaGvZhKnn1Zfa1JVnp7vkMV4DYVxPLWBVoN65L"
+        );
+        expect(err.programErrorStack.map((pk) => pk.toString())).to.deep.equal([
+          "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS",
+          "HmbTLCmaGvZhKnn1Zfa1JVnp7vkMV4DYVxPLWBVoN65L",
+        ]);
         return true;
       }
     );
@@ -855,8 +876,11 @@ describe("Lockup and Registry", () => {
         await tryEndUnstake();
       },
       (err) => {
-        assert.equal(err.code, 6009);
-        assert.equal(err.msg, "The unstake timelock has not yet expired.");
+        assert.equal(err.error.errorCode.number, 6009);
+        assert.equal(
+          err.error.errorMessage,
+          "The unstake timelock has not yet expired."
+        );
         return true;
       }
     );

+ 1 - 1
tests/misc/Anchor.toml

@@ -14,4 +14,4 @@ program = "./target/deploy/misc.so"
 exclude = ["programs/shared"]
 
 [scripts]
-test = "yarn run mocha -t 1000000 tests/"
+test = "yarn run ts-mocha -t 1000000 tests/*.ts"

+ 91 - 53
tests/misc/tests/misc.js → tests/misc/tests/misc.ts

@@ -1,18 +1,20 @@
-const anchor = require("@project-serum/anchor");
-const assert = require("assert");
-const {
-  ASSOCIATED_TOKEN_PROGRAM_ID,
-  TOKEN_PROGRAM_ID,
-  Token,
-} = require("@solana/spl-token");
-const miscIdl = require("../target/idl/misc.json");
-const {
-  SystemProgram,
-  Keypair,
+import * as anchor from "@project-serum/anchor";
+import { Program, BN, IdlAccounts, AnchorError } from "@project-serum/anchor";
+import {
   PublicKey,
+  Keypair,
+  SystemProgram,
   SYSVAR_RENT_PUBKEY,
-} = require("@solana/web3.js");
+} from "@solana/web3.js";
+import {
+  TOKEN_PROGRAM_ID,
+  Token,
+  ASSOCIATED_TOKEN_PROGRAM_ID,
+} from "@solana/spl-token";
+import { Misc } from "../target/types/misc";
 const utf8 = anchor.utils.bytes.utf8;
+const assert = require("assert");
+const miscIdl = require("../target/idl/misc.json");
 
 describe("misc", () => {
   // Configure the client to use the local cluster.
@@ -231,9 +233,8 @@ describe("misc", () => {
       assert.ok(false);
     } catch (err) {
       const errMsg = "A close constraint was violated";
-      assert.equal(err.toString(), errMsg);
-      assert.equal(err.msg, errMsg);
-      assert.equal(err.code, 2011);
+      assert.equal(err.error.errorMessage, errMsg);
+      assert.equal(err.error.errorCode.number, 2011);
     }
   });
 
@@ -702,7 +703,7 @@ describe("misc", () => {
           });
         },
         (err) => {
-          assert.equal(err.code, 2009);
+          assert.equal(err.error.errorCode.number, 2009);
           return true;
         }
       );
@@ -737,7 +738,7 @@ describe("misc", () => {
           });
         },
         (err) => {
-          assert.equal(err.code, 2015);
+          assert.equal(err.error.errorCode.number, 2015);
           return true;
         }
       );
@@ -872,7 +873,7 @@ describe("misc", () => {
         },
       }),
       (err) => {
-        assert.equal(err.code, 2006);
+        assert.equal(err.error.errorCode.number, 2006);
         return true;
       }
     );
@@ -990,8 +991,10 @@ describe("misc", () => {
         },
       });
       assert.ok(false);
-    } catch (err) {
-      assert.equal(err.code, 2004);
+    } catch (_err) {
+      assert.ok(_err instanceof AnchorError);
+      const err: AnchorError = _err;
+      assert.equal(err.error.errorCode.number, 2004);
     }
   });
 
@@ -1027,8 +1030,10 @@ describe("misc", () => {
         },
       });
       assert.ok(false);
-    } catch (err) {
-      assert.equal(err.code, 2006);
+    } catch (_err) {
+      assert.ok(_err instanceof AnchorError);
+      const err: AnchorError = _err;
+      assert.equal(err.error.errorCode.number, 2006);
     }
   });
 
@@ -1054,8 +1059,10 @@ describe("misc", () => {
         signers: [newAcc],
       });
       assert.ok(false);
-    } catch (err) {
-      assert.equal(err.code, 2019);
+    } catch (_err) {
+      assert.ok(_err instanceof AnchorError);
+      const err: AnchorError = _err;
+      assert.equal(err.error.errorCode.number, 2019);
     }
   });
 
@@ -1086,8 +1093,10 @@ describe("misc", () => {
         signers: [mint],
       });
       assert.ok(false);
-    } catch (err) {
-      assert.equal(err.code, 2016);
+    } catch (_err) {
+      assert.ok(_err instanceof AnchorError);
+      const err: AnchorError = _err;
+      assert.equal(err.error.errorCode.number, 2016);
     }
   });
 
@@ -1118,8 +1127,10 @@ describe("misc", () => {
         signers: [mint],
       });
       assert.ok(false);
-    } catch (err) {
-      assert.equal(err.code, 2017);
+    } catch (_err) {
+      assert.ok(_err instanceof AnchorError);
+      const err: AnchorError = _err;
+      assert.equal(err.error.errorCode.number, 2017);
     }
   });
 
@@ -1150,8 +1161,10 @@ describe("misc", () => {
         signers: [mint],
       });
       assert.ok(false);
-    } catch (err) {
-      assert.equal(err.code, 2018);
+    } catch (_err) {
+      assert.ok(_err instanceof AnchorError);
+      const err: AnchorError = _err;
+      assert.equal(err.error.errorCode.number, 2018);
     }
   });
 
@@ -1195,8 +1208,10 @@ describe("misc", () => {
         signers: [token],
       });
       assert.ok(false);
-    } catch (err) {
-      assert.equal(err.code, 2015);
+    } catch (_err) {
+      assert.ok(_err instanceof AnchorError);
+      const err: AnchorError = _err;
+      assert.equal(err.error.errorCode.number, 2015);
     }
   });
 
@@ -1252,8 +1267,10 @@ describe("misc", () => {
         signers: [token],
       });
       assert.ok(false);
-    } catch (err) {
-      assert.equal(err.code, 2014);
+    } catch (_err) {
+      assert.ok(_err instanceof AnchorError);
+      const err: AnchorError = _err;
+      assert.equal(err.error.errorCode.number, 2014);
     }
   });
 
@@ -1303,8 +1320,10 @@ describe("misc", () => {
         },
       });
       assert.ok(false);
-    } catch (err) {
-      assert.equal(err.code, 2015);
+    } catch (_err) {
+      assert.ok(_err instanceof AnchorError);
+      const err: AnchorError = _err;
+      assert.equal(err.error.errorCode.number, 2015);
     }
   });
 
@@ -1366,8 +1385,10 @@ describe("misc", () => {
         },
       });
       assert.ok(false);
-    } catch (err) {
-      assert.equal(err.code, 2014);
+    } catch (_err) {
+      assert.ok(_err instanceof AnchorError);
+      const err: AnchorError = _err;
+      assert.equal(err.error.errorCode.number, 2014);
     }
   });
 
@@ -1430,8 +1451,10 @@ describe("misc", () => {
         },
       });
       assert.ok(false);
-    } catch (err) {
-      assert.equal(err.code, 3014);
+    } catch (_err) {
+      assert.ok(_err instanceof AnchorError);
+      const err: AnchorError = _err;
+      assert.equal(err.error.errorCode.number, 3014);
     }
   });
 
@@ -1456,8 +1479,10 @@ describe("misc", () => {
         signers: [data],
       });
       assert.ok(false);
-    } catch (err) {
-      assert.equal(err.code, 2005);
+    } catch (_err) {
+      assert.ok(_err instanceof AnchorError);
+      const err: AnchorError = _err;
+      assert.equal(err.error.errorCode.number, 2005);
     }
   });
 
@@ -1582,9 +1607,14 @@ describe("misc", () => {
         },
       });
       assert.ok(false);
-    } catch (err) {
-      assert.equal(2005, err.code);
-      assert.equal("A rent exempt constraint was violated", err.msg);
+    } catch (_err) {
+      assert.ok(_err instanceof AnchorError);
+      const err: AnchorError = _err;
+      assert.equal(err.error.errorCode.number, 2005);
+      assert.equal(
+        "A rent exemption constraint was violated",
+        err.error.errorMessage
+      );
     }
   });
 
@@ -1611,8 +1641,10 @@ describe("misc", () => {
           },
         });
         assert.ok(false);
-      } catch (err) {
-        assert.equal(err.code, 2006);
+      } catch (_err) {
+        assert.ok(_err instanceof AnchorError);
+        const err: AnchorError = _err;
+        assert.equal(err.error.errorCode.number, 2006);
       }
 
       // matching bump seed for wrong address but derived from wrong program
@@ -1624,8 +1656,10 @@ describe("misc", () => {
           },
         });
         assert.ok(false);
-      } catch (err) {
-        assert.equal(err.code, 2006);
+      } catch (_err) {
+        assert.ok(_err instanceof AnchorError);
+        const err: AnchorError = _err;
+        assert.equal(err.error.errorCode.number, 2006);
       }
 
       // correct inputs should lead to successful tx
@@ -1661,8 +1695,10 @@ describe("misc", () => {
           },
         });
         assert.ok(false);
-      } catch (err) {
-        assert.equal(err.code, 2006);
+      } catch (_err) {
+        assert.ok(_err instanceof AnchorError);
+        const err: AnchorError = _err;
+        assert.equal(err.error.errorCode.number, 2006);
       }
 
       // same seeds but derived from wrong program
@@ -1674,8 +1710,10 @@ describe("misc", () => {
           },
         });
         assert.ok(false);
-      } catch (err) {
-        assert.equal(err.code, 2006);
+      } catch (_err) {
+        assert.ok(_err instanceof AnchorError);
+        const err: AnchorError = _err;
+        assert.equal(err.error.errorCode.number, 2006);
       }
 
       // correct inputs should lead to successful tx

+ 10 - 0
tests/misc/tsconfig.json

@@ -0,0 +1,10 @@
+{
+    "compilerOptions": {
+        "types": ["mocha", "chai"],
+        "typeRoots": ["./node_modules/@types"],
+        "lib": ["es2015"],
+        "module": "commonjs",
+        "target": "es6",
+        "esModuleInterop": true
+    }
+}

+ 2 - 0
tests/package.json

@@ -42,6 +42,8 @@
   "devDependencies": {
     "@types/node": "^14.14.37",
     "chai": "^4.3.4",
+    "@types/chai": "^4.3.0",
+    "@types/mocha": "^9.1.0",
     "mocha": "^9.1.3",
     "ts-mocha": "^8.0.0",
     "typescript": "^4.4.4",

+ 2 - 3
tests/system-accounts/tests/system-accounts.js

@@ -52,9 +52,8 @@ describe("system_accounts", () => {
       assert.ok(false);
     } catch (err) {
       const errMsg = "The given account is not owned by the system program";
-      assert.equal(err.toString(), errMsg);
-      assert.equal(err.msg, errMsg);
-      assert.equal(err.code, 3011);
+      assert.equal(err.error.errorMessage, errMsg);
+      assert.equal(err.error.errorCode.number, 3011);
     }
   });
 });

+ 3 - 4
tests/sysvars/tests/sysvars.js

@@ -18,7 +18,7 @@ describe("sysvars", () => {
     console.log("Your transaction signature", tx);
   });
 
-  it("Fails when the wrote pubkeys are provided", async () => {
+  it("Fails when the wrong pubkeys are provided", async () => {
     try {
       await program.methods
         .sysvars()
@@ -31,9 +31,8 @@ describe("sysvars", () => {
       assert.ok(false);
     } catch (err) {
       const errMsg = "The given public key does not match the required sysvar";
-      assert.strictEqual(err.toString(), errMsg);
-      assert.strictEqual(err.msg, errMsg);
-      assert.strictEqual(err.code, 3015);
+      assert.strictEqual(err.error.errorMessage, errMsg);
+      assert.strictEqual(err.error.errorCode.number, 3015);
     }
   });
 });

+ 10 - 0
tests/yarn.lock

@@ -153,6 +153,11 @@
   dependencies:
     "@types/node" "*"
 
+"@types/chai@^4.3.0":
+  version "4.3.0"
+  resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.0.tgz#23509ebc1fa32f1b4d50d6a66c4032d5b8eaabdc"
+  integrity sha512-/ceqdqeRraGolFTcfoXNiqjyQhZzbINDngeoAq9GoHa8PPK1yNzTaxWjA6BFWp5Ua9JpXEMSS4s5i9tS0hOJtw==
+
 "@types/connect@^3.4.33":
   version "3.4.35"
   resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1"
@@ -179,6 +184,11 @@
   resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.176.tgz#641150fc1cda36fbfa329de603bbb175d7ee20c0"
   integrity sha512-xZmuPTa3rlZoIbtDUyJKZQimJV3bxCmzMIO2c9Pz9afyDro6kr7R79GwcB6mRhuoPmV2p1Vb66WOJH7F886WKQ==
 
+"@types/mocha@^9.1.0":
+  version "9.1.0"
+  resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.0.tgz#baf17ab2cca3fcce2d322ebc30454bff487efad5"
+  integrity sha512-QCWHkbMv4Y5U9oW10Uxbr45qMMSzl4OzijsozynUAgx3kEHUdXB00udx2dWDQ7f2TU2a2uuiFaRZjCe3unPpeg==
+
 "@types/node@*":
   version "16.11.7"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.7.tgz#36820945061326978c42a01e56b61cd223dfdc42"

+ 2 - 2
ts/package.json

@@ -34,7 +34,7 @@
   },
   "dependencies": {
     "@project-serum/borsh": "^0.2.5",
-    "@solana/web3.js": "^1.17.0",
+    "@solana/web3.js": "^1.36.0",
     "base64-js": "^1.5.1",
     "bn.js": "^5.1.2",
     "bs58": "^4.0.1",
@@ -59,7 +59,7 @@
     "@types/bn.js": "^4.11.6",
     "@types/bs58": "^4.0.1",
     "@types/crypto-hash": "^1.1.2",
-    "@types/jest": "^27.0.3",
+    "@types/jest": "^27.4.1",
     "@types/pako": "^1.0.1",
     "@typescript-eslint/eslint-plugin": "^4.6.0",
     "@typescript-eslint/parser": "^4.6.0",

+ 1 - 2
ts/src/coder/spl-token/buffer-layout.ts

@@ -2,7 +2,6 @@ import BN from "bn.js";
 import * as BufferLayout from "buffer-layout";
 import { Layout } from "buffer-layout";
 import { PublicKey } from "@solana/web3.js";
-import * as utils from "../../utils";
 
 export function uint64(property?: string): Layout<u64 | null> {
   return new WrappedLayout(
@@ -88,7 +87,7 @@ export class COptionLayout<T> extends Layout<T | null> {
     } else if (discriminator === 1) {
       return this.layout.decode(b, offset + 4);
     }
-    throw new Error("Invalid coption " + this.property);
+    throw new Error("Invalid coption " + this.layout.property);
   }
 
   getSpan(b: Buffer, offset = 0): number {

+ 260 - 5
ts/src/error.ts

@@ -1,3 +1,6 @@
+import { PublicKey } from "@solana/web3.js";
+import * as features from "./utils/features.js";
+
 export class IdlError extends Error {
   constructor(message: string) {
     super(message);
@@ -5,10 +8,212 @@ export class IdlError extends Error {
   }
 }
 
+interface ErrorCode {
+  code: string;
+  number: number;
+}
+
+interface FileLine {
+  file: string;
+  line: number;
+}
+
+type Origin = string | FileLine;
+type ComparedAccountNames = [string, string];
+type ComparedPublicKeys = [PublicKey, PublicKey];
+type ComparedValues = ComparedAccountNames | ComparedPublicKeys;
+
+export class ProgramErrorStack {
+  constructor(readonly stack: PublicKey[]) {}
+
+  public static parse(logs: string[]) {
+    const programKeyRegex = /^Program (\w*) invoke/;
+    const successRegex = /^Program \w* success/;
+
+    const programStack: PublicKey[] = [];
+    for (let i = 0; i < logs.length; i++) {
+      if (successRegex.exec(logs[i])) {
+        programStack.pop();
+        continue;
+      }
+
+      const programKey = programKeyRegex.exec(logs[i])?.[1];
+      if (!programKey) {
+        continue;
+      }
+      programStack.push(new PublicKey(programKey));
+    }
+    return new ProgramErrorStack(programStack);
+  }
+}
+
+export class AnchorError extends Error {
+  readonly error: {
+    errorCode: ErrorCode;
+    errorMessage: string;
+    comparedValues?: ComparedValues;
+    origin?: Origin;
+  };
+  private readonly _programErrorStack: ProgramErrorStack;
+
+  constructor(
+    errorCode: ErrorCode,
+    errorMessage: string,
+    readonly errorLogs: string[],
+    readonly logs: string[],
+    origin?: Origin,
+    comparedValues?: ComparedValues
+  ) {
+    super(errorLogs.join("\n").replace("Program log: ", ""));
+    this.error = { errorCode, errorMessage, comparedValues, origin };
+    this._programErrorStack = ProgramErrorStack.parse(logs);
+  }
+
+  public static parse(logs: string[]) {
+    if (!logs) {
+      return null;
+    }
+
+    const anchorErrorLogIndex = logs.findIndex((log) =>
+      log.startsWith("Program log: AnchorError")
+    );
+    if (anchorErrorLogIndex === -1) {
+      return null;
+    }
+    const anchorErrorLog = logs[anchorErrorLogIndex];
+    const errorLogs = [anchorErrorLog];
+    let comparedValues: ComparedValues | undefined;
+    if (anchorErrorLogIndex + 1 < logs.length) {
+      // This catches the comparedValues where the following is logged
+      // <AnchorError>
+      // Left:
+      // <Pubkey>
+      // Right:
+      // <Pubkey>
+      if (logs[anchorErrorLogIndex + 1] === "Program log: Left:") {
+        const pubkeyRegex = /^Program log: (.*)$/;
+        const leftPubkey = pubkeyRegex.exec(logs[anchorErrorLogIndex + 2])![1];
+        const rightPubkey = pubkeyRegex.exec(logs[anchorErrorLogIndex + 4])![1];
+        comparedValues = [
+          new PublicKey(leftPubkey),
+          new PublicKey(rightPubkey),
+        ];
+        errorLogs.push(
+          ...logs.slice(anchorErrorLogIndex + 1, anchorErrorLogIndex + 5)
+        );
+      }
+      // This catches the comparedValues where the following is logged
+      // <AnchorError>
+      // Left: <value>
+      // Right: <value>
+      else if (logs[anchorErrorLogIndex + 1].startsWith("Program log: Left:")) {
+        const valueRegex = /^Program log: (Left|Right): (.*)$/;
+        const leftValue = valueRegex.exec(logs[anchorErrorLogIndex + 1])![2];
+        const rightValue = valueRegex.exec(logs[anchorErrorLogIndex + 2])![2];
+        errorLogs.push(
+          ...logs.slice(anchorErrorLogIndex + 1, anchorErrorLogIndex + 3)
+        );
+        comparedValues = [leftValue, rightValue];
+      }
+    }
+    const regexNoInfo = /^Program log: AnchorError occurred\. Error Code: (.*)\. Error Number: (\d*)\. Error Message: (.*)\./;
+    const noInfoAnchorErrorLog = regexNoInfo.exec(anchorErrorLog);
+    const regexFileLine = /^Program log: AnchorError thrown in (.*):(\d*)\. Error Code: (.*)\. Error Number: (\d*)\. Error Message: (.*)\./;
+    const fileLineAnchorErrorLog = regexFileLine.exec(anchorErrorLog);
+    const regexAccountName = /^Program log: AnchorError caused by account: (.*)\. Error Code: (.*)\. Error Number: (\d*)\. Error Message: (.*)\./;
+    const accountNameAnchorErrorLog = regexAccountName.exec(anchorErrorLog);
+    if (noInfoAnchorErrorLog) {
+      const [
+        errorCodeString,
+        errorNumber,
+        errorMessage,
+      ] = noInfoAnchorErrorLog.slice(1, 4);
+      const errorCode = {
+        code: errorCodeString,
+        number: parseInt(errorNumber),
+      };
+      return new AnchorError(
+        errorCode,
+        errorMessage,
+        errorLogs,
+        logs,
+        undefined,
+        comparedValues
+      );
+    } else if (fileLineAnchorErrorLog) {
+      const [
+        file,
+        line,
+        errorCodeString,
+        errorNumber,
+        errorMessage,
+      ] = fileLineAnchorErrorLog.slice(1, 6);
+      const errorCode = {
+        code: errorCodeString,
+        number: parseInt(errorNumber),
+      };
+      const fileLine = { file, line: parseInt(line) };
+      return new AnchorError(
+        errorCode,
+        errorMessage,
+        errorLogs,
+        logs,
+        fileLine,
+        comparedValues
+      );
+    } else if (accountNameAnchorErrorLog) {
+      const [
+        accountName,
+        errorCodeString,
+        errorNumber,
+        errorMessage,
+      ] = accountNameAnchorErrorLog.slice(1, 5);
+      const origin = accountName;
+      const errorCode = {
+        code: errorCodeString,
+        number: parseInt(errorNumber),
+      };
+      return new AnchorError(
+        errorCode,
+        errorMessage,
+        errorLogs,
+        logs,
+        origin,
+        comparedValues
+      );
+    } else {
+      return null;
+    }
+  }
+
+  get program(): PublicKey {
+    return this._programErrorStack.stack[
+      this._programErrorStack.stack.length - 1
+    ];
+  }
+
+  get programErrorStack(): PublicKey[] {
+    return this._programErrorStack.stack;
+  }
+
+  public toString(): string {
+    return this.message;
+  }
+}
+
 // An error from a user defined program.
 export class ProgramError extends Error {
-  constructor(readonly code: number, readonly msg: string, ...params: any[]) {
-    super(...params);
+  private readonly _programErrorStack?: ProgramErrorStack;
+
+  constructor(
+    readonly code: number,
+    readonly msg: string,
+    readonly logs?: string[]
+  ) {
+    super();
+    if (logs) {
+      this._programErrorStack = ProgramErrorStack.parse(logs);
+    }
   }
 
   public static parse(
@@ -44,24 +249,71 @@ export class ProgramError extends Error {
     // Parse user error.
     let errorMsg = idlErrors.get(errorCode);
     if (errorMsg !== undefined) {
-      return new ProgramError(errorCode, errorMsg, errorCode + ": " + errorMsg);
+      return new ProgramError(errorCode, errorMsg, err.logs);
     }
 
     // Parse framework internal error.
     errorMsg = LangErrorMessage.get(errorCode);
     if (errorMsg !== undefined) {
-      return new ProgramError(errorCode, errorMsg, errorCode + ": " + errorMsg);
+      return new ProgramError(errorCode, errorMsg, err.logs);
     }
 
     // Unable to parse the error. Just return the untranslated error.
     return null;
   }
 
+  get program(): PublicKey | undefined {
+    return this._programErrorStack?.stack[
+      this._programErrorStack.stack.length - 1
+    ];
+  }
+
+  get programErrorStack(): PublicKey[] | undefined {
+    return this._programErrorStack?.stack;
+  }
+
   public toString(): string {
     return this.msg;
   }
 }
 
+export function translateError(err: any, idlErrors: Map<number, string>) {
+  if (features.isSet("debug-logs")) {
+    console.log("Translating error:", err);
+  }
+
+  const anchorError = AnchorError.parse(err.logs);
+  if (anchorError) {
+    return anchorError;
+  }
+
+  const programError = ProgramError.parse(err, idlErrors);
+  if (programError) {
+    return programError;
+  }
+  if (err.logs) {
+    const handler = {
+      get: function (target, prop) {
+        if (prop === "programErrorStack") {
+          return target.programErrorStack.stack;
+        } else if (prop === "program") {
+          return target.programErrorStack.stack[
+            err.programErrorStack.stack.length - 1
+          ];
+        } else {
+          // this is the normal way to return all other props
+          // without modifying them.
+          // @ts-expect-error
+          return Reflect.get(...arguments);
+        }
+      },
+    };
+    err.programErrorStack = ProgramErrorStack.parse(err.logs);
+    return new Proxy(err, handler);
+  }
+  return err;
+}
+
 const LangErrorCode = {
   // Instructions.
   InstructionMissing: 100,
@@ -166,7 +418,10 @@ const LangErrorMessage = new Map([
   [LangErrorCode.ConstraintSigner, "A signer constraint was violated"],
   [LangErrorCode.ConstraintRaw, "A raw constraint was violated"],
   [LangErrorCode.ConstraintOwner, "An owner constraint was violated"],
-  [LangErrorCode.ConstraintRentExempt, "A rent exempt constraint was violated"],
+  [
+    LangErrorCode.ConstraintRentExempt,
+    "A rent exemption constraint was violated",
+  ],
   [LangErrorCode.ConstraintSeeds, "A seeds constraint was violated"],
   [LangErrorCode.ConstraintExecutable, "An executable constraint was violated"],
   [LangErrorCode.ConstraintState, "A state constraint was violated"],

+ 3 - 13
ts/src/program/namespace/rpc.ts

@@ -3,8 +3,7 @@ import Provider from "../../provider.js";
 import { Idl } from "../../idl.js";
 import { splitArgsAndCtx } from "../context.js";
 import { TransactionFn } from "./transaction.js";
-import { ProgramError } from "../../error.js";
-import * as features from "../../utils/features.js";
+import { translateError } from "../../error.js";
 import {
   AllInstructions,
   InstructionContextFn,
@@ -22,18 +21,9 @@ export default class RpcFactory {
       const tx = txFn(...args);
       const [, ctx] = splitArgsAndCtx(idlIx, [...args]);
       try {
-        const txSig = await provider.send(tx, ctx.signers, ctx.options);
-        return txSig;
+        return await provider.send(tx, ctx.signers, ctx.options);
       } catch (err) {
-        if (features.isSet("debug-logs")) {
-          console.log("Translating error:", err);
-        }
-
-        let translatedErr = ProgramError.parse(err, idlErrors);
-        if (translatedErr === null) {
-          throw err;
-        }
-        throw translatedErr;
+        throw translateError(err, idlErrors);
       }
     };
 

+ 2 - 11
ts/src/program/namespace/simulate.ts

@@ -9,8 +9,7 @@ import { TransactionFn } from "./transaction.js";
 import { EventParser, Event } from "../event.js";
 import { Coder } from "../../coder/index.js";
 import { Idl, IdlEvent } from "../../idl.js";
-import { ProgramError } from "../../error.js";
-import * as features from "../../utils/features.js";
+import { translateError } from "../../error.js";
 import {
   AllInstructions,
   IdlTypes,
@@ -37,15 +36,7 @@ export default class SimulateFactory {
       try {
         resp = await provider!.simulate(tx, ctx.signers, ctx.options);
       } catch (err) {
-        if (features.isSet("debug-logs")) {
-          console.log("Translating error:", err);
-        }
-
-        let translatedErr = ProgramError.parse(err, idlErrors);
-        if (translatedErr === null) {
-          throw err;
-        }
-        throw translatedErr;
+        throw translateError(err, idlErrors);
       }
       if (resp === undefined) {
         throw new Error("Unable to simulate transaction");

+ 65 - 8
ts/src/provider.ts

@@ -5,11 +5,12 @@ import {
   Transaction,
   TransactionSignature,
   ConfirmOptions,
-  sendAndConfirmRawTransaction,
   RpcResponseAndContext,
   SimulatedTransactionResponse,
   Commitment,
+  SendTransactionError,
 } from "@solana/web3.js";
+import { bs58 } from "./utils/bytes/index.js";
 import { isBrowser } from "./utils/common.js";
 
 /**
@@ -115,13 +116,30 @@ export default class Provider {
 
     const rawTx = tx.serialize();
 
-    const txId = await sendAndConfirmRawTransaction(
-      this.connection,
-      rawTx,
-      opts
-    );
-
-    return txId;
+    try {
+      return await sendAndConfirmRawTransaction(this.connection, rawTx, opts);
+    } catch (err) {
+      // thrown if the underlying 'confirmTransaction' encounters a failed tx
+      // the 'confirmTransaction' error does not return logs so we make another rpc call to get them
+      if (err instanceof ConfirmError) {
+        // choose the shortest available commitment for 'getTransaction'
+        // (the json RPC does not support any shorter than "confirmed" for 'getTransaction')
+        // because that will see the tx sent with `sendAndConfirmRawTransaction` no matter which
+        // commitment `sendAndConfirmRawTransaction` used
+        const failedTx = await this.connection.getTransaction(
+          bs58.encode(tx.signature!),
+          { commitment: "confirmed" }
+        );
+        if (!failedTx) {
+          throw err;
+        } else {
+          const logs = failedTx.meta?.logMessages;
+          throw !logs ? err : new SendTransactionError(err.message, logs);
+        }
+      } else {
+        throw err;
+      }
+    }
   }
 
   /**
@@ -253,6 +271,45 @@ async function simulateTransaction(
   return res.result;
 }
 
+// Copy of Connection.sendAndConfirmRawTransaction that throws
+// a better error if 'confirmTransaction` returns an error status
+async function sendAndConfirmRawTransaction(
+  connection: Connection,
+  rawTransaction: Buffer,
+  options?: ConfirmOptions
+): Promise<TransactionSignature> {
+  const sendOptions = options && {
+    skipPreflight: options.skipPreflight,
+    preflightCommitment: options.preflightCommitment || options.commitment,
+  };
+
+  const signature = await connection.sendRawTransaction(
+    rawTransaction,
+    sendOptions
+  );
+
+  const status = (
+    await connection.confirmTransaction(
+      signature,
+      options && options.commitment
+    )
+  ).value;
+
+  if (status.err) {
+    throw new ConfirmError(
+      `Raw transaction ${signature} failed (${JSON.stringify(status)})`
+    );
+  }
+
+  return signature;
+}
+
+class ConfirmError extends Error {
+  constructor(message?: string) {
+    super(message);
+  }
+}
+
 /**
  * Sets the default provider on the client.
  */

+ 332 - 0
ts/tests/error.spec.ts

@@ -0,0 +1,332 @@
+import { ProgramErrorStack, AnchorError } from "../src/error";
+
+describe("ProgramErrorStack", () => {
+  test("basic", () => {
+    const logs = [
+      "Program ERRM6YCMsccM22TEaPuu35KVU4iCY3GLCz4qMsKLYReE invoke [1]",
+      "Program ERRM6YCMsccM22TEaPuu35KVU4iCY3GLCz4qMsKLYReE consumed 3797 of 200000 compute units",
+      "Program ERRM6YCMsccM22TEaPuu35KVU4iCY3GLCz4qMsKLYReE failed: custom program error: 0x29",
+    ];
+
+    expect(
+      ProgramErrorStack.parse(logs).stack.map((publicKey) =>
+        publicKey.toString()
+      )
+    ).toEqual(["ERRM6YCMsccM22TEaPuu35KVU4iCY3GLCz4qMsKLYReE"]);
+  });
+
+  it("basic multiple ix", () => {
+    const logs = [
+      "Program 9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin invoke [1]",
+      "Program 9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin consumed 4308 of 200000 compute units",
+      "Program 9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin success",
+      "Program ERRM6YCMsccM22TEaPuu35KVU4iCY3GLCz4qMsKLYReE invoke [1]",
+      "Program ERRM6YCMsccM22TEaPuu35KVU4iCY3GLCz4qMsKLYReE consumed 3797 of 200000 compute units",
+      "Program ERRM6YCMsccM22TEaPuu35KVU4iCY3GLCz4qMsKLYReE failed: custom program error: 0x29",
+    ];
+
+    expect(
+      ProgramErrorStack.parse(logs).stack.map((publicKey) =>
+        publicKey.toString()
+      )
+    ).toEqual(["ERRM6YCMsccM22TEaPuu35KVU4iCY3GLCz4qMsKLYReE"]);
+  });
+
+  it("failed inner ix", () => {
+    const logs = [
+      "Program Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS invoke [1]",
+      "Program log: Instruction: Create",
+      "Program ERRM6YCMsccM22TEaPuu35KVU4iCY3GLCz4qMsKLYReE invoke [2]",
+      "Program log: AnchorError thrown in programs/switchboard_v2/src/actions/aggregator_save_result_action.rs:235. Error Code: OracleMismatchError. Error Number: 6021. Error Message: An unexpected oracle account was provided for the transaction..",
+      "Program Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS consumed 12619 of 1400000 compute units",
+      "Program Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS failed: Program failed to complete",
+    ];
+
+    expect(
+      ProgramErrorStack.parse(logs).stack.map((publicKey) =>
+        publicKey.toString()
+      )
+    ).toEqual([
+      "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS",
+      "ERRM6YCMsccM22TEaPuu35KVU4iCY3GLCz4qMsKLYReE",
+    ]);
+  });
+
+  it("ignore successful inner ix", () => {
+    const logs = [
+      "Program Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS invoke [1]",
+      "Program log: Instruction: Create",
+      "Program 11111111111111111111111111111111 invoke [2]",
+      "Program 11111111111111111111111111111111 success",
+      "Program log: panicked at programs/floats/src/lib.rs:17:9",
+      "Program Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS consumed 12619 of 1400000 compute units",
+      "Program failed to complete: BPF program panicked",
+      "Program Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS failed: Program failed to complete",
+    ];
+    expect(
+      ProgramErrorStack.parse(logs).stack.map((publicKey) =>
+        publicKey.toString()
+      )
+    ).toEqual(["Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"]);
+  });
+
+  it("ignore successful inner ix but don't ignore failing inner ix", () => {
+    const logs = [
+      "Program Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS invoke [1]",
+      "Program log: Instruction: Create",
+      "Program 11111111111111111111111111111111 invoke [2]",
+      "Program 11111111111111111111111111111111 success",
+      "Program ERRM6YCMsccM22TEaPuu35KVU4iCY3GLCz4qMsKLYReE invoke [2]",
+      "Program log: panicked at programs/floats/src/lib.rs:17:9",
+      "Program Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS consumed 12619 of 1400000 compute units",
+      "Program failed to complete: BPF program panicked",
+      "Program Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS failed: Program failed to complete",
+    ];
+    expect(
+      ProgramErrorStack.parse(logs).stack.map((publicKey) =>
+        publicKey.toString()
+      )
+    ).toEqual([
+      "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS",
+      "ERRM6YCMsccM22TEaPuu35KVU4iCY3GLCz4qMsKLYReE",
+    ]);
+  });
+
+  it("ignore successful inner ix but don't ignore failing inner ix - big nested", () => {
+    const logs = [
+      "Program Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS invoke [1]",
+      "Program log: Instruction: Create",
+      "Program 11111111111111111111111111111111 invoke [2]",
+      "Program 11111111111111111111111111111111 success",
+      "Program 1119iqpxV28XnisGGQVMHsABdWZAx9PjtwegepRhGm5 invoke [2]",
+      "Program 1119iqpxV28XnisGGQVMHsABdWZAx9PjtwegepRhGm5 consumed 4308 of 200000 compute units",
+      "Program 1119iqpxV28XnisGGQVMHsABdWZAx9PjtwegepRhGm5 success",
+      "Program 222fsxyjMZSSpT9gpucChbiFmjZC2GtaZmKsBkh66KMZ invoke [2]",
+      "Program 333fE7qebyWBjcaCJcVmkzwrheA1Ka9bjGChuhVD9iQr invoke [3]",
+      "Program 444D5MLf9UbeJBiuFw5WzVG3bMejweunZHPboWm2oTsh invoke [4]",
+      "Program 444D5MLf9UbeJBiuFw5WzVG3bMejweunZHPboWm2oTsh consumed 14343 of 200000 compute units",
+      "Program 444D5MLf9UbeJBiuFw5WzVG3bMejweunZHPboWm2oTsh success",
+      "Program 555CBVR14jAYjK8jRE5kurBACiSNYXkffciRSG2R3krX invoke [4]",
+      "Program 555CBVR14jAYjK8jRE5kurBACiSNYXkffciRSG2R3krX consumed 163337 of 200000 compute units",
+      "Program 555CBVR14jAYjK8jRE5kurBACiSNYXkffciRSG2R3krX success",
+      "Program 666UBGVHWNP7qNqUdnYz86owJ8oErztVvgeF5Dd5v8cR invoke [4]",
+      "Program 666UBGVHWNP7qNqUdnYz86owJ8oErztVvgeF5Dd5v8cR success",
+      "Program 333fE7qebyWBjcaCJcVmkzwrheA1Ka9bjGChuhVD9iQr success",
+      "Program 222fsxyjMZSSpT9gpucChbiFmjZC2GtaZmKsBkh66KMZ success",
+      "Program 777UGK3pU4ygVWwnn7MDnetec1nSVg4Xi53DFSHu9D6A invoke [2]",
+      "Program 888E49S65VpyDmydi6juT7tsSwNyD3ZEVkV8te1rL3iH invoke [3]",
+      "Program 999X95icuyGzfYoeP6SPMb8aMn6ahfCpAt9VPddSNPPi invoke [4]",
+      "Program 999X95icuyGzfYoeP6SPMb8aMn6ahfCpAt9VPddSNPPi success",
+      "Program ERRM6YCMsccM22TEaPuu35KVU4iCY3GLCz4qMsKLYReE invoke [4]",
+      "Program log: panicked at programs/floats/src/lib.rs:17:9",
+      "Program Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS consumed 12619 of 1400000 compute units",
+      "Program failed to complete: BPF program panicked",
+      "Program Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS failed: Program failed to complete",
+    ];
+
+    expect(
+      ProgramErrorStack.parse(logs).stack.map((publicKey) =>
+        publicKey.toString()
+      )
+    ).toEqual([
+      "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS",
+      "777UGK3pU4ygVWwnn7MDnetec1nSVg4Xi53DFSHu9D6A",
+      "888E49S65VpyDmydi6juT7tsSwNyD3ZEVkV8te1rL3iH",
+      "ERRM6YCMsccM22TEaPuu35KVU4iCY3GLCz4qMsKLYReE",
+    ]);
+  });
+});
+
+describe("AnchorError", () => {
+  it("FileLine AnchorError with Pubkeys", () => {
+    const logs = [
+      "Program SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f invoke [1]",
+      "Program log: Instruction: AggregatorSaveResult",
+      "Program log: AnchorError thrown in programs/switchboard_v2/src/actions/aggregator_save_result_action.rs:235. Error Code: OracleMismatchError. Error Number: 6021. Error Message: An unexpected oracle account was provided for the transaction..",
+      "Program log: Left:",
+      "Program log: Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS",
+      "Program log: Right:",
+      "Program log: SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f",
+      "Program SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f consumed 28928 of 200000 compute units",
+      "Program SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f failed: custom program error: 0x1785",
+    ];
+
+    const anchorError = AnchorError.parse(logs)!;
+    expect(anchorError.program.toString()).toEqual(
+      "SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f"
+    );
+    expect(anchorError.error.errorCode).toEqual({
+      code: "OracleMismatchError",
+      number: 6021,
+    });
+    expect(anchorError.error.errorMessage).toEqual(
+      "An unexpected oracle account was provided for the transaction."
+    );
+    expect(anchorError.error.origin).toEqual({
+      file:
+        "programs/switchboard_v2/src/actions/aggregator_save_result_action.rs",
+      line: 235,
+    });
+    expect(
+      anchorError.error.comparedValues!.map((pk) => pk.toString())
+    ).toEqual([
+      "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS",
+      "SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f",
+    ]);
+    expect(
+      anchorError.programErrorStack!.map((publicKey) => publicKey.toString())
+    ).toEqual(["SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f"]);
+    expect(anchorError.errorLogs).toEqual([
+      "Program log: AnchorError thrown in programs/switchboard_v2/src/actions/aggregator_save_result_action.rs:235. Error Code: OracleMismatchError. Error Number: 6021. Error Message: An unexpected oracle account was provided for the transaction..",
+      "Program log: Left:",
+      "Program log: Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS",
+      "Program log: Right:",
+      "Program log: SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f",
+    ]);
+  });
+
+  it("FileLine AnchorError with Values", () => {
+    const logs = [
+      "Program SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f invoke [1]",
+      "Program log: Instruction: AggregatorSaveResult",
+      "Program log: AnchorError thrown in programs/switchboard_v2/src/actions/aggregator_save_result_action.rs:235. Error Code: OracleMismatchError. Error Number: 6021. Error Message: An unexpected oracle account was provided for the transaction..",
+      "Program log: Left: 1337",
+      "Program log: Right: 4220",
+      "Program SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f consumed 28928 of 200000 compute units",
+      "Program SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f failed: custom program error: 0x1785",
+    ];
+
+    const anchorError = AnchorError.parse(logs)!;
+    expect(anchorError.program.toString()).toEqual(
+      "SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f"
+    );
+    expect(anchorError.error.errorCode).toEqual({
+      code: "OracleMismatchError",
+      number: 6021,
+    });
+    expect(anchorError.error.errorMessage).toEqual(
+      "An unexpected oracle account was provided for the transaction."
+    );
+    expect(anchorError.error.origin).toEqual({
+      file:
+        "programs/switchboard_v2/src/actions/aggregator_save_result_action.rs",
+      line: 235,
+    });
+    expect(anchorError.error.comparedValues!).toEqual(["1337", "4220"]);
+    expect(
+      anchorError.programErrorStack!.map((publicKey) => publicKey.toString())
+    ).toEqual(["SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f"]);
+    expect(anchorError.errorLogs).toEqual([
+      "Program log: AnchorError thrown in programs/switchboard_v2/src/actions/aggregator_save_result_action.rs:235. Error Code: OracleMismatchError. Error Number: 6021. Error Message: An unexpected oracle account was provided for the transaction..",
+      "Program log: Left: 1337",
+      "Program log: Right: 4220",
+    ]);
+  });
+
+  it("AccountName AnchorError with Pubkeys", () => {
+    const logs = [
+      "Program SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f invoke [1]",
+      "Program log: Instruction: AggregatorSaveResult",
+      "Program log: AnchorError caused by account: some_account. Error Code: OracleMismatchError. Error Number: 6021. Error Message: An unexpected oracle account was provided for the transaction..",
+      "Program log: Left:",
+      "Program log: Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS",
+      "Program log: Right:",
+      "Program log: SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f",
+      "Program SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f consumed 28928 of 200000 compute units",
+      "Program SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f failed: custom program error: 0x1785",
+    ];
+
+    const anchorError = AnchorError.parse(logs)!;
+    expect(anchorError.program.toString()).toEqual(
+      "SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f"
+    );
+    expect(anchorError.error.errorCode).toEqual({
+      code: "OracleMismatchError",
+      number: 6021,
+    });
+    expect(anchorError.error.errorMessage).toEqual(
+      "An unexpected oracle account was provided for the transaction."
+    );
+    expect(anchorError.error.origin).toEqual("some_account");
+    expect(
+      anchorError.error.comparedValues!.map((pk) => pk.toString())
+    ).toEqual([
+      "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS",
+      "SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f",
+    ]);
+    expect(
+      anchorError.programErrorStack!.map((publicKey) => publicKey.toString())
+    ).toEqual(["SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f"]);
+    expect(anchorError.errorLogs).toEqual([
+      "Program log: AnchorError caused by account: some_account. Error Code: OracleMismatchError. Error Number: 6021. Error Message: An unexpected oracle account was provided for the transaction..",
+      "Program log: Left:",
+      "Program log: Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS",
+      "Program log: Right:",
+      "Program log: SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f",
+    ]);
+  });
+
+  it("AccountName AnchorError with Values", () => {
+    const logs = [
+      "Program SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f invoke [1]",
+      "Program log: Instruction: AggregatorSaveResult",
+      "Program log: AnchorError caused by account: some_account. Error Code: OracleMismatchError. Error Number: 6021. Error Message: An unexpected oracle account was provided for the transaction..",
+      "Program log: Left: 1337",
+      "Program log: Right: 4220",
+      "Program SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f consumed 28928 of 200000 compute units",
+      "Program SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f failed: custom program error: 0x1785",
+    ];
+
+    const anchorError = AnchorError.parse(logs)!;
+    expect(anchorError.program.toString()).toEqual(
+      "SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f"
+    );
+    expect(anchorError.error.errorCode).toEqual({
+      code: "OracleMismatchError",
+      number: 6021,
+    });
+    expect(anchorError.error.errorMessage).toEqual(
+      "An unexpected oracle account was provided for the transaction."
+    );
+    expect(anchorError.error.origin).toEqual("some_account");
+    expect(anchorError.error.comparedValues!).toEqual(["1337", "4220"]);
+    expect(
+      anchorError.programErrorStack!.map((publicKey) => publicKey.toString())
+    ).toEqual(["SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f"]);
+    expect(anchorError.errorLogs).toEqual([
+      "Program log: AnchorError caused by account: some_account. Error Code: OracleMismatchError. Error Number: 6021. Error Message: An unexpected oracle account was provided for the transaction..",
+      "Program log: Left: 1337",
+      "Program log: Right: 4220",
+    ]);
+  });
+
+  it("Empty AnchorError", () => {
+    const logs = [
+      "Program SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f invoke [1]",
+      "Program log: Instruction: AggregatorSaveResult",
+      "Program log: AnchorError occurred. Error Code: OracleMismatchError. Error Number: 6021. Error Message: An unexpected oracle account was provided for the transaction..",
+      "Program SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f consumed 28928 of 200000 compute units",
+      "Program SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f failed: custom program error: 0x1785",
+    ];
+
+    const anchorError = AnchorError.parse(logs)!;
+    expect(anchorError.program.toString()).toEqual(
+      "SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f"
+    );
+    expect(anchorError.error.errorCode).toEqual({
+      code: "OracleMismatchError",
+      number: 6021,
+    });
+    expect(anchorError.error.errorMessage).toEqual(
+      "An unexpected oracle account was provided for the transaction."
+    );
+    expect(anchorError.error.origin).toBeUndefined();
+    expect(anchorError.error.comparedValues).toBeUndefined();
+    expect(
+      anchorError.programErrorStack!.map((publicKey) => publicKey.toString())
+    ).toEqual(["SW1TCH7qEPTdLsDHRgPuMQjbQxKdH2aBStViMFnt64f"]);
+    expect(anchorError.errorLogs).toEqual([
+      "Program log: AnchorError occurred. Error Code: OracleMismatchError. Error Number: 6021. Error Message: An unexpected oracle account was provided for the transaction..",
+    ]);
+  });
+});

+ 92 - 17
ts/yarn.lock

@@ -649,6 +649,27 @@
     minimatch "^3.0.4"
     strip-json-comments "^3.1.1"
 
+"@ethersproject/bytes@^5.6.0":
+  version "5.6.0"
+  resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.6.0.tgz#81652f2a0e04533575befadce555213c11d8aa20"
+  integrity sha512-3hJPlYemb9V4VLfJF5BfN0+55vltPZSHU3QKUyP9M3Y2TcajbiRrz65UG+xVHOzBereB1b9mn7r12o177xgN7w==
+  dependencies:
+    "@ethersproject/logger" "^5.6.0"
+
+"@ethersproject/logger@^5.6.0":
+  version "5.6.0"
+  resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.6.0.tgz#d7db1bfcc22fd2e4ab574cba0bb6ad779a9a3e7a"
+  integrity sha512-BiBWllUROH9w+P21RzoxJKzqoqpkyM1pRnEKG69bulE9TSQD8SAIvTQqIMZmmCO8pUNkgLP1wndX1gKghSpBmg==
+
+"@ethersproject/sha2@^5.5.0":
+  version "5.6.0"
+  resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.6.0.tgz#364c4c11cc753bda36f31f001628706ebadb64d9"
+  integrity sha512-1tNWCPFLu1n3JM9t4/kytz35DkuF9MxqkGGEHNauEbaARdm2fafnOyw1s0tIQDPKF/7bkP1u3dbrmjpn5CelyA==
+  dependencies:
+    "@ethersproject/bytes" "^5.6.0"
+    "@ethersproject/logger" "^5.6.0"
+    hash.js "1.1.7"
+
 "@istanbuljs/load-nyc-config@^1.0.0":
   version "1.1.0"
   resolved "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz"
@@ -927,21 +948,28 @@
   dependencies:
     "@sinonjs/commons" "^1.7.0"
 
-"@solana/web3.js@^1.17.0":
-  version "1.17.0"
-  resolved "https://registry.npmjs.org/@solana/web3.js/-/web3.js-1.17.0.tgz"
-  integrity sha512-PBOHY260CudciLwBgwt1U8upwCS1Jq0BbS6EVyX0tz6Tj14Dp4i87dQNyntentNiGQQ+yWBIk4vJEm+PMCSd/A==
+"@solana/buffer-layout@^3.0.0":
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/@solana/buffer-layout/-/buffer-layout-3.0.0.tgz#b9353caeb9a1589cb77a1b145bcb1a9a93114326"
+  integrity sha512-MVdgAKKL39tEs0l8je0hKaXLQFb7Rdfb0Xg2LjFZd8Lfdazkg6xiS98uAZrEKvaoF3i4M95ei9RydkGIDMeo3w==
+  dependencies:
+    buffer "~6.0.3"
+
+"@solana/web3.js@^1.36.0":
+  version "1.36.0"
+  resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.36.0.tgz#79d7d5217b49b80139f4de68953adc5b9a9a264f"
+  integrity sha512-RNT1451iRR7TyW7EJKMCrH/0OXawIe4zVm0DWQASwXlR/u1jmW6FrmH0lujIh7cGTlfOVbH+2ZU9AVUPLBFzwA==
   dependencies:
     "@babel/runtime" "^7.12.5"
+    "@ethersproject/sha2" "^5.5.0"
+    "@solana/buffer-layout" "^3.0.0"
     bn.js "^5.0.0"
     borsh "^0.4.0"
     bs58 "^4.0.1"
     buffer "6.0.1"
-    buffer-layout "^1.2.0"
-    crypto-hash "^1.2.2"
+    cross-fetch "^3.1.4"
     jayson "^3.4.4"
     js-sha3 "^0.8.0"
-    node-fetch "^2.6.1"
     rpc-websockets "^7.4.2"
     secp256k1 "^4.0.2"
     superstruct "^0.14.2"
@@ -1069,12 +1097,12 @@
   dependencies:
     "@types/istanbul-lib-report" "*"
 
-"@types/jest@^27.0.3":
-  version "27.0.3"
-  resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.0.3.tgz#0cf9dfe9009e467f70a342f0f94ead19842a783a"
-  integrity sha512-cmmwv9t7gBYt7hNKH5Spu7Kuu/DotGa+Ff+JGRKZ4db5eh8PnKS4LuebJ3YLUoyOyIHraTGyULn23YtEAm0VSg==
+"@types/jest@^27.4.1":
+  version "27.4.1"
+  resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.4.1.tgz#185cbe2926eaaf9662d340cc02e548ce9e11ab6d"
+  integrity sha512-23iPJADSmicDVrWk+HT58LMJtzLAnB2AgIzplQuq/bSrGaxCrlvRFjGbXmamnnk/mAmCdLStiGqggu28ocUyiw==
   dependencies:
-    jest-diff "^27.0.0"
+    jest-matcher-utils "^27.0.0"
     pretty-format "^27.0.0"
 
 "@types/json-schema@^7.0.3":
@@ -1568,6 +1596,14 @@ buffer@6.0.1:
     base64-js "^1.3.1"
     ieee754 "^1.2.1"
 
+buffer@~6.0.3:
+  version "6.0.3"
+  resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6"
+  integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==
+  dependencies:
+    base64-js "^1.3.1"
+    ieee754 "^1.2.1"
+
 bufferutil@^4.0.1:
   version "4.0.3"
   resolved "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.3.tgz"
@@ -1836,7 +1872,7 @@ create-require@^1.1.0:
   resolved "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz"
   integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
 
-cross-fetch@^3.1.5:
+cross-fetch@^3.1.4, cross-fetch@^3.1.5:
   version "3.1.5"
   resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f"
   integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==
@@ -1852,7 +1888,7 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3:
     shebang-command "^2.0.0"
     which "^2.0.1"
 
-crypto-hash@*, crypto-hash@^1.2.2, crypto-hash@^1.3.0:
+crypto-hash@*, crypto-hash@^1.3.0:
   version "1.3.0"
   resolved "https://registry.npmjs.org/crypto-hash/-/crypto-hash-1.3.0.tgz"
   integrity sha512-lyAZ0EMyjDkVvz8WOeVnuCPvKVBXcMv1l5SVqO1yC7PzTwrD/pPje/BIRbWhMoPe436U+Y2nD7f5bFx0kt+Sbg==
@@ -1964,6 +2000,11 @@ diff-sequences@^27.0.6:
   resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.0.6.tgz#3305cb2e55a033924054695cc66019fd7f8e5723"
   integrity sha512-ag6wfpBFyNXZ0p8pcuIDS//D8H062ZQJ3fzYxjpmeKjnz8W4pekL3AI8VohmyZmsWW2PWaHgjsmqR6L13101VQ==
 
+diff-sequences@^27.5.1:
+  version "27.5.1"
+  resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327"
+  integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==
+
 diff@^4.0.1:
   version "4.0.2"
   resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz"
@@ -2569,7 +2610,7 @@ has@^1.0.3:
   dependencies:
     function-bind "^1.1.1"
 
-hash.js@^1.0.0, hash.js@^1.0.3:
+hash.js@1.1.7, hash.js@^1.0.0, hash.js@^1.0.3:
   version "1.1.7"
   resolved "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz"
   integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==
@@ -2976,7 +3017,7 @@ jest-config@27.3.1, jest-config@^27.3.1:
     micromatch "^4.0.4"
     pretty-format "^27.3.1"
 
-jest-diff@^27.0.0, jest-diff@^27.3.1:
+jest-diff@^27.3.1:
   version "27.3.1"
   resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.3.1.tgz#d2775fea15411f5f5aeda2a5e02c2f36440f6d55"
   integrity sha512-PCeuAH4AWUo2O5+ksW4pL9v5xJAcIKPUPfIhZBcG1RKv/0+dvaWTQK1Nrau8d67dp65fOqbeMdoil+6PedyEPQ==
@@ -2986,6 +3027,16 @@ jest-diff@^27.0.0, jest-diff@^27.3.1:
     jest-get-type "^27.3.1"
     pretty-format "^27.3.1"
 
+jest-diff@^27.5.1:
+  version "27.5.1"
+  resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.5.1.tgz#a07f5011ac9e6643cf8a95a462b7b1ecf6680def"
+  integrity sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==
+  dependencies:
+    chalk "^4.0.0"
+    diff-sequences "^27.5.1"
+    jest-get-type "^27.5.1"
+    pretty-format "^27.5.1"
+
 jest-docblock@^27.0.6:
   version "27.0.6"
   resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-27.0.6.tgz#cc78266acf7fe693ca462cbbda0ea4e639e4e5f3"
@@ -3034,6 +3085,11 @@ jest-get-type@^27.3.1:
   resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.3.1.tgz#a8a2b0a12b50169773099eee60a0e6dd11423eff"
   integrity sha512-+Ilqi8hgHSAdhlQ3s12CAVNd8H96ZkQBfYoXmArzZnOfAtVAJEiPDBirjByEblvG/4LPJmkL+nBqPO3A1YJAEg==
 
+jest-get-type@^27.5.1:
+  version "27.5.1"
+  resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1"
+  integrity sha512-2KY95ksYSaK7DMBWQn6dQz3kqAf3BB64y2udeG+hv4KfSOb9qwcYQstTJc1KCbsix+wLZWZYN8t7nwX3GOBLRw==
+
 jest-haste-map@^27.3.1:
   version "27.3.1"
   resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.3.1.tgz#7656fbd64bf48bda904e759fc9d93e2c807353ee"
@@ -3086,6 +3142,16 @@ jest-leak-detector@^27.3.1:
     jest-get-type "^27.3.1"
     pretty-format "^27.3.1"
 
+jest-matcher-utils@^27.0.0:
+  version "27.5.1"
+  resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz#9c0cdbda8245bc22d2331729d1091308b40cf8ab"
+  integrity sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw==
+  dependencies:
+    chalk "^4.0.0"
+    jest-diff "^27.5.1"
+    jest-get-type "^27.5.1"
+    pretty-format "^27.5.1"
+
 jest-matcher-utils@^27.3.1:
   version "27.3.1"
   resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.3.1.tgz#257ad61e54a6d4044e080d85dbdc4a08811e9c1c"
@@ -3746,7 +3812,7 @@ node-addon-api@^2.0.0:
   resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-2.0.2.tgz"
   integrity sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==
 
-node-fetch@2.6.7, node-fetch@^2.6.1:
+node-fetch@2.6.7:
   version "2.6.7"
   resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
   integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
@@ -4006,6 +4072,15 @@ pretty-format@^27.0.0, pretty-format@^27.3.1:
     ansi-styles "^5.0.0"
     react-is "^17.0.1"
 
+pretty-format@^27.5.1:
+  version "27.5.1"
+  resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e"
+  integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==
+  dependencies:
+    ansi-regex "^5.0.1"
+    ansi-styles "^5.0.0"
+    react-is "^17.0.1"
+
 process-nextick-args@~2.0.0:
   version "2.0.1"
   resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz"