Created
February 15, 2019 21:54
-
-
Save tomquisel/a5b24f33df7b2b39a95a21a97ac94eaa to your computer and use it in GitHub Desktop.
convert Stellar XDR to minimal JSON object
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// adapted from: https://github.com/stellar/laboratory/blob/master/src/utilities/extrapolateFromXdr.js | |
import * as _ from "lodash"; | |
import { xdr, StrKey, Keypair, Operation } from "stellar-sdk"; | |
/** | |
* Convert XDR string to minimal JSON object | |
* @param input input xdr string | |
* @param type type of XDR (TransactionResult, TransactionMeta, etc...) | |
*/ | |
export function extrapolateFromXdr(input: string, type: string) { | |
// TODO: Check to see if type exists | |
// TODO: input validation | |
let xdrObject; | |
try { | |
// @ts-ignore | |
xdrObject = xdr[type].fromXDR(input, "base64"); | |
} catch (error) { | |
throw new Error("Input XDR could not be parsed"); | |
} | |
return buildTreeFromObject(xdrObject, type); | |
} | |
function buildTreeFromObject(object: any, name: string) { | |
if (_.isArray(object)) { | |
return parseArray(object); | |
} else if (!hasChildren(object)) { | |
if (!name) { | |
throw Error(`Values must know their names: ${JSON.stringify(object)}`); | |
} | |
return getValue(object, name); | |
} else if (object.switch) { | |
return parseArm(object); | |
} else { | |
return parseNormal(object); | |
} | |
} | |
function parseArray(array: any[]): any[] { | |
return array.map((item, index) => | |
buildTreeFromObject(item, index.toString()), | |
); | |
} | |
function parseArm(object: any): object { | |
const name = object.switch().name; | |
if (_.isString(object.arm())) { | |
return { | |
[name]: buildTreeFromObject(object[object.arm()](), name), | |
}; | |
} | |
return name; | |
} | |
function parseNormal(object: any) { | |
const res: any = {}; | |
_(object) | |
.functionsIn() | |
.without("toXDR") | |
.value() | |
.forEach((name) => { | |
res[name] = buildTreeFromObject(object[name](), name); | |
}); | |
return res; | |
} | |
function hasChildren(object: any) { | |
// string | |
if (_.isString(object)) { | |
return false; | |
} | |
// node buffer | |
if (object && (object._isBuffer || Buffer.isBuffer(object))) { | |
return false; | |
} | |
const functions = _.functionsIn(object); | |
if (functions.length === 0) { | |
return false; | |
} | |
// int64 | |
if ( | |
_.includes(functions, "getLowBits") && | |
_.includes(functions, "getHighBits") | |
) { | |
return false; | |
} | |
return true; | |
} | |
const amountFields = [ | |
"amount", | |
"startingBalance", | |
"sendMax", | |
"destAmount", | |
"limit", | |
]; | |
function getValue(object: any, name: string) { | |
if (_.includes(amountFields, name)) { | |
// @ts-ignore | |
return Operation._fromXDRAmount(object); | |
} | |
if (name === "hint") { | |
// strkey encoding is using base32 encoding. Encoded public key consists of: | |
// | |
// * 1 byte version byte (0x30 encoded as `G`) | |
// * 32 bytes public key | |
// * 2 bytes checksum | |
// | |
// Because base32 symbols are 5-bit, more than one symbol is needed to represent a single byte. | |
// Signature Hint is the last 4 bytes of the public key. So we need to try to show as many 5-bit | |
// chunks as possible included between bytes 30 and 33 (included). | |
// | |
// byte 1: ##### ### | |
// byte 2: ## ##### # | |
// byte 3: #### #### | |
// byte 4: # ##### ## | |
// byte 5: ### ##### <---------- 40 bits / full alignment | |
// byte 6: ##### ### | |
// byte 7: ## ##### # | |
// | |
// ..... | |
// | |
// byte 26: ##### ### | |
// byte 27: ## ##### # | |
// byte 28: #### #### full b32 symbols | |
// byte 29: # ##### ## |--------------------------| | |
// byte 30: ### 48### | | |
// byte 31: Signature Hint start | 49### 50# | Signature Hint end | |
// byte 32: ## 51### 5 | | | |
// byte 33: 2### 53## | |
// byte 34: # 54### 55 | |
// byte 35: ### 56### | |
// | |
const hintBytes = new Buffer(object, "base64"); | |
const partialPublicKey = Buffer.concat([new Buffer(28).fill(0), hintBytes]); | |
const keypair = new Keypair({ | |
type: "ed25519", | |
// @ts-ignore | |
publicKey: partialPublicKey, | |
}); | |
const partialPublicKeyString = | |
"G" + | |
new Buffer(46).fill("_").toString() + | |
keypair.publicKey().substr(47, 5) + | |
new Buffer(4).fill("_").toString(); | |
return partialPublicKeyString; | |
} | |
if (name === "ed25519" || name === "publicKeyTypeEd25519") { | |
const address = StrKey.encodeEd25519PublicKey(object); | |
return address; | |
} | |
if (name === "assetCode" || name === "assetCode4" || name === "assetCode12") { | |
return (object.toString() as string).replace(/\0/g, ""); | |
} | |
if (object && (object._isBuffer || Buffer.isBuffer(object))) { | |
return new Buffer(object).toString("base64"); | |
} | |
if (typeof object === "undefined") { | |
return; | |
} | |
// getValue is a leaf in the recursive xdr extrapolating function meaning that | |
// whatever this function returns will be in the final result as-is. | |
// Therefore, we want them in string format so that it displayable in React. | |
// One example of why we need this is that UnsignedHyper values won't get | |
// displayed unless we convert it to a string. | |
if (typeof object.toString === "function") { | |
return object.toString(); | |
} | |
throw new Error( | |
"Internal laboratory bug: Encountered value type in XDR viewer that does not have a toString method", | |
); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment