SHOWING: First 60
mint","dep":"07555bcf148efa97cfed8717011e0f1429bf7a667d916faa6ca2d1cadd8a8ec2i0","tick":"GROK42","blk":"428940"}#65,414,133textmint","dep":"07555bcf148efa97cfed8717011e0f1429bf7a667d916faa6ca2d1cadd8a8ec2i0","tick":"GROK42","blk":"428940"}#65,414,124textdmt-mint.grok42#65,369,661tapdmt-mint.grok42#65,369,660tapdmt-mint.grok42#65,369,659tapdmt-mint.grok42#65,369,657tapdmt-mint.grok42#65,369,656tapdmt-mint.grok42#65,369,655tapdmt-mint.grok42#65,369,654tapdmt-mint.grok42#65,369,653tapdmt-mint.grok42#65,369,651tapdmt-mint.grok42#65,369,650tapdmt-mint.grok42#65,369,649tapdmt-mint.grok42#65,369,648tapdmt-mint.grok42#65,364,137tapdmt-mint.grok42#65,364,136tap#64,449,585"재할 것이다."
#: /workspaces/ord_ko/docs/src/overview.md:208
msgid "Exotics"
msgstr "엑소틱"
#: /workspaces/ord_ko/docs/src/overview.md:211
msgid ""
"Satoshis may be prized for reasons other than their name or rarity. This "
"might be due to a quality of the number itself, like having an integer "
"square or cube root. Or it might be due to a connection to a historical "
"event, such as satoshis from block 477,120, the block in which SegWit "
"activated, or 2099999997689999°, the last satoshi that will ever be mined."
msgstr ""
"사토시는 이름이나 희귀성 이외의 이유로 소중히 여겨질 수 있다. 정수 제곱근이"
"나 세제곱근과 같은 숫자 자체의 특성 때문일 수도 있다. 또는 세그윗이 활성화된 "
"블록인 477,120 블록의 사토시나 마지막으로 채굴될 사토시인 2099999997689999°"
"와 같은 역사적 사건과의 연관성 때문일 수도 있다."
#: /workspaces/ord_ko/docs/src/overview.md:217
msgid ""
"Such satoshis are termed \"exotic\". Which satoshis are exotic and what "
"makes them so is subjective. Ordinal theorists are encouraged to seek out "
"exotics based on criteria of their own devising."
msgstr ""
"이러한 사토시를 “엑소틱”(이색품)이라고 한다. 어떤 사토시가 엑소틱인지, 그리"
#44,987,334text"because nobody can own it."
msgstr ""
"디지털 아티팩트는 소유자가 있을 수 있다. 숫자는 누구도 소유할 수 없으므로 디"
"지털 아티팩트가 아니다."
#: /workspaces/ord_ko/docs/src/digital-artifacts.md:27
msgid ""
"Digital artifacts are complete. An NFT that points to off-chain content on "
"IPFS or Arweave is incomplete, and thus not a digital artifact."
msgstr ""
"디지털 아티팩트는 완전하다. IPFS 또는 Arweave의 오프체인 콘텐츠를 가리키는 "
"NFT는 불완전하며, 따라서 디지털 아티팩트가 아니다."
#: /workspaces/ord_ko/docs/src/digital-artifacts.md:30
msgid ""
"Digital artifacts are permissionless. An NFT which cannot be sold without "
"paying a royalty is not permissionless, and thus not a digital artifact."
msgstr ""
"디지털 아티팩트는 무허가성이라는 특성을 가지고 있다. 로열티를 지불하지 않고"
"는 판매할 수 없는 NFT는 무허가성을 지니지 않으며, 따라서 디지털 아티팩트가 아"
"니다."
#: /workspaces/ord_ko/docs/src/digital-artifacts.md:33
msgid ""
"Digital artifacts are uncensorable. Perhaps you can change a database entry "
"on a centralized ledger today, but maybe not tomorrow, and thus one cannot "
"be a digital artifact."
msgstr ""
"디지털 아티팩트는 검열할 수 없다. 중앙 원장의 데이터베이스 항목을 오늘은 변경"
#44,987,330text"널스는 역사적으로 흥미롭지 않다."
#: /workspaces/ord_ko/docs/src/overview.md:247
msgid ""
"In another sense though, ordinals were in fact created by Satoshi Nakamoto "
"in 2009 when he mined the Bitcoin genesis block. In this sense, ordinals, "
"and especially early ordinals, are certainly of historical interest."
msgstr ""
"하지만 다른 의미에서 오디널스는 사실 2009년 사토시 나카모토가 비트코인 제네시"
"스 블록을 채굴할 때 만들어졌다. 이런 관점에서 오디널스, 특히 초기 오디널스는 "
"분명 역사적으로 흥미로운 존재다."
#: /workspaces/ord_ko/docs/src/overview.md:251
msgid ""
"Many ordinal theorists favor the latter view. This is not least because the "
"ordinals were independently discovered on at least two separate occasions, "
"long before the era of modern NFTs began."
msgstr ""
"많은 오디널 이론가들은 후자의 견해를 선호한다. 이는 특히 오디널스가 현대적 "
"NFT 시대가 시작되기 훨씬 전에 적어도 두 차례에 걸쳐 독립적으로 발견되었기 때"
"문이다."
#: /workspaces/ord_ko/docs/src/overview.md:255
msgid ""
"On August 21st, 2012, Charlie Lee [posted a proposal to add proof-of-stake "
"to Bitcoin to the Bitcoin Talk forum](https://bitcointalk.org/index.php?"
"topic=102355.0). This wasn't an asset scheme, but did use the ordinal "
#44,987,329textmsgstr ""
"인스크립션 콘텐츠 모델은 웹의 콘텐츠 모델이다. 인스크립션은 MIME 유형이라고"
"도 하는 콘텐츠 유형과 바이트 문자열(string)인 콘텐츠 자체로 구성된다. 이를 통"
"해 웹 서버에서 인스크립션 콘텐츠를 반환하고 다른 인스크립션의 콘텐츠를 사용 "
"및 리믹스하는 HTML 인스크립션을 만들 수 있다."
#: /workspaces/ord_ko/docs/src/inscriptions.md:21
msgid ""
"Inscription content is entirely on-chain, stored in taproot script-path "
"spend scripts. Taproot scripts have very few restrictions on their content, "
"and additionally receive the witness discount, making inscription content "
"storage relatively economical."
msgstr ""
"인스크립션 콘텐츠는 전적으로 온체인에 저장되며, 탭루트 스크립트 경로 지출 스"
"크립트에 저장된다. 탭루트 스크립트는 콘텐츠에 대한 제한이 거의 없으며, 추가"
"로 증인 할인(witness discount)을 받을 수 있어 인스크립션 콘텐츠 저장 비용이 "
"상대적으로 저렴하다."
#: /workspaces/ord_ko/docs/src/inscriptions.md:26
msgid ""
"Since taproot script spends can only be made from existing taproot outputs, "
"inscriptions are made using a two-phase commit/reveal procedure. First, in "
"the commit transaction, a taproot output committing to a script containing "
"the inscription content is created. Second, in the reveal transaction, the "
#44,987,326text"`Content-Security-Policy` headers."
msgstr ""
"이는 HTML 및 SVG 인스크립션을 ‘sandbox’ 속성을 사용하여 'iframe' 내에 로드하"
"고 `Content-Security-Policy` 헤더를 사용하여 인스크립션 콘텐츠를 제공함으로"
"써 수행된다."
#: /workspaces/ord_ko/docs/src/inscriptions/metadata.md:4
msgid ""
"Inscriptions may include [CBOR](https://cbor.io/) metadata, stored as data "
"pushes in fields with tag `5`. Since data pushes are limited to 520 bytes, "
"metadata longer than 520 bytes must be split into multiple tag `5` fields, "
"which will then be concatenated before decoding."
msgstr ""
#: /workspaces/ord_ko/docs/src/inscriptions/metadata.md:9
msgid ""
"Metadata is human readable, and all metadata will be displayed to the user "
"with its inscription. Inscribers are encouraged to consider how metadata "
"will be displayed, and make metadata concise and attractive."
msgstr ""
#: /workspaces/ord_ko/docs/src/inscriptions/metadata.md:13
msgid "Metadata is rendered to HTML for display as follows:"
msgstr ""
#: /workspaces/ord_ko/docs/src/inscriptions/metadata.md:15
msgid ""
#44,987,323textmsgid "Inscription IDs"
msgstr "인스크립션 ID"
#: /workspaces/ord_ko/docs/src/inscriptions.md:99
msgid ""
"The inscriptions are contained within the inputs of a reveal transaction. In "
"order to uniquely identify them they are assigned an ID of the form:"
msgstr ""
"인스크립션은 리빌 트랜잭션의 입력값에 포함되어 있다. 인스크립션을 고유하게 식"
"별하기 위해 인스크립션에는 이 형식의 ID가 할당된다:"
#: /workspaces/ord_ko/docs/src/inscriptions.md:102
msgid "`521f8eccffa4c41a3a7728dd012ea5a4a02feed81f41159231251ecf1e5c79dai0`"
msgstr "`521f8eccffa4c41a3a7728dd012ea5a4a02feed81f41159231251ecf1e5c79dai0`"
#: /workspaces/ord_ko/docs/src/inscriptions.md:104
msgid ""
"The part in front of the `i` is the transaction ID (`txid`) of the reveal "
"transaction. The number after the `i` defines the index (starting at 0) of "
"new inscriptions being inscribed in the reveal transaction."
msgstr ""
"`i` 앞 부분은 리빌 트랜잭션의 트랜잭션 ID(`txid`)이다. `i` 뒤의 숫자는 리빌 "
"트랜잭션에 새겨지는 새 인스크립션의 인덱스(0부터 시작)를 정의한다."
#: /workspaces/ord_ko/docs/src/inscriptions.md:108
msgid ""
"Inscriptions can either be located in different inputs, within the same "
#44,987,313textmsgid ""
"…can only be changed by you. If you were a trader, and you made your way to "
"18th century China, none but you could stamp it with your chop-mark."
msgstr ""
"...당신에 의해서만 변화될 수 있다. 만약 당신이 상인이고 18세기 중국으로 갔었"
"다면, 그 동전에 각인(chop-mark)을 찍을 수 있는 사람은 당신밖에 없었을 것이다."
#: /workspaces/ord_ko/docs/src/digital-artifacts.md:15
msgid ""
"…can only be disposed of by you. The sale, trade, or gift is yours to make, "
"to whomever you wish."
msgstr ""
"...당신에 의해서만 처분될 수 있다. 당신이 원하는 누구에게나 판매, 거래 또는 "
"선물 할 수 있다."
#: /workspaces/ord_ko/docs/src/digital-artifacts.md:18
msgid ""
"What are digital artifacts? Simply put, they are the digital equivalent of "
"physical artifacts."
msgstr ""
"디지털 아티팩트(인공물)란 무엇인가? 간단히 말해, 이는 물리적 인공물의 디지털 "
"버전이다."
#: /workspaces/ord_ko/docs/src/digital-artifacts.md:21
msgid ""
"For a digital thing to be a digital artifact, it must be like that coin of "
"yours:"
msgstr ""
"디지털 한 것이 디지털 아티팩트가 되려면 당신의 그 동전과 같은 것이어야 한다:"
#: /workspaces/ord_ko/docs/src/digital-artifacts.md:24
msgid ""
"Digital artifacts can have owners. A number is not a digital artifact, "
#44,987,308textmsgid ""
"These inscribed sats can then be transferred using bitcoin transactions, "
"sent to bitcoin addresses, and held in bitcoin UTXOs. These transactions, "
"addresses, and UTXOs are normal bitcoin transactions, addresses, and UTXOS "
"in all respects, with the exception that in order to send individual sats, "
"transactions must control the order and value of inputs and outputs "
"according to ordinal theory."
msgstr ""
"이렇게 새겨진 SAT는 비트코인 트랜잭션을 사용해 전송하고, 비트코인 주소로 보내"
"고, 비트코인 UTXO에 보관할 수 있다. 이러한 트랜잭션, 주소, UTXO는 모든 면에"
"서 일반적인 비트코인 트랜잭션, 주소, UTXOs와 동일다. 다만 개별 사토시를 전송"
"하기 위해서는 트랜잭션이 오디널 이론에 따라 입력과 출력의 순서와 값을 제어해"
"야 한다."
#: /workspaces/ord_ko/docs/src/inscriptions.md:15
msgid ""
"The inscription content model is that of the web. An inscription consists of "
"a content type, also known as a MIME type, and the content itself, which is "
"a byte string. This allows inscription content to be returned from a web "
"server, and for creating HTML inscriptions that use and remix the content of "
"other inscriptions."
#44,987,287text"inscription content to be served by a web server and viewed in a web browser."
msgstr ""
"인스크립션의 데이터 모델은 웹 서버에서 인스크립션 콘텐츠를 제공하고 웹 브라우"
"저에서 볼 수 있도록 하는 HTTP 응답의 데이터 모델이다."
#: /workspaces/ord_ko/docs/src/inscriptions.md:73
msgid "Fields"
msgstr "필드"
#: /workspaces/ord_ko/docs/src/inscriptions.md:76
msgid ""
"Inscriptions may include fields before an optional body. Each field consists "
"of two data pushes, a tag and a value."
msgstr ""
"인스크립션은 optional body 앞에 필드를 포함할 수 있다. 각 필드는 두 개의 데이"
"터 푸시, 즉 태그 및 값으로 구성된다."
#: /workspaces/ord_ko/docs/src/inscriptions.md:79
msgid ""
"Currently, the only defined field is `content-type`, with a tag of `1`, "
"whose value is the MIME type of the body."
msgstr ""
"현재 정의된 유일한 필드는 'content-type'이며, 태그는 '1'이며, 값은 body에 "
"MIME 유형이다."
#: /workspaces/ord_ko/docs/src/inscriptions.md:82
msgid ""
"The beginning of the body and end of fields is indicated with an empty data "
"push."
msgstr "Body의 시작과 필드의 끝은 빈 데이터 푸시로 표시된다."
#: /workspaces/ord_ko/docs/src/inscriptions.md:85
msgid ""
#44,987,284text"sequence of events set in motion with the mining of the first block, so many "
"years ago."
msgstr ""
"이러한 오디널의 독립적인 발명은 어떻게 보면 오디널이 발명된 것이 아니라 발견"
"되었거나 재발견되었음을 나타낸다. 오디널은 비트코인의 수학의 필연적인 결과"
"로, 현대의 기록이 아니라 고대의 기원에서 비롯된 것이다. 오디널은 수년 전 첫 "
"번째 블록의 채굴과 함께 시작된 일련의 사건의 정점이다."
#: /workspaces/ord_ko/docs/src/digital-artifacts.md:4
msgid ""
"Imagine a physical artifact. A rare coin, say, held safe for untold years in "
"the dark, secret clutch of a Viking hoard, now dug from the earth by your "
"grasping hands. It…"
msgstr ""
"물리적 인공물을 상상해 보라. 예를 들어, 밝혀지지 않은 세월 동안 어둡고 은밀"
"한 바이킹 창고의 손아귀에 있던 희귀한 동전이 이제 여러분의 손으로 의해 땅속에"
"서 파헤쳐졌다. 이 동전은…"
#: /workspaces/ord_ko/docs/src/digital-artifacts.md:8
msgid ""
"…has an owner. You. As long as you keep it safe, nobody can take it from you."
msgstr ""
"...주인이 있다. 바로 당신. 당신이 안전하게 보관하는 한 누구도 빼앗을 수 없다."
#: /workspaces/ord_ko/docs/src/digital-artifacts.md:10
msgid "…is complete. It has no missing parts."
msgstr "...완전하다. 결여된 요소가 없다."
#: /workspaces/ord_ko/docs/src/digital-artifacts.md:12
#44,987,280text"Unrecognized tags are interpreted differently depending on whether they are "
"even or odd, following the \"it's okay to be odd\" rule used by the "
"Lightning Network."
msgstr ""
"인식되지 않는 태그는 라이트닝 네트워크에서 사용하는 \"홀수여도 괜찮아\" 규칙"
"에 따라 짝수인지 홀수인지에 따라 다르게 해석된다."
#: /workspaces/ord_ko/docs/src/inscriptions.md:89
msgid ""
"Even tags are used for fields which may affect creation, initial assignment, "
"or transfer of an inscription. Thus, inscriptions with unrecognized even "
"fields must be displayed as \"unbound\", that is, without a location."
msgstr ""
"인스크립션 생성, 초기 할당 또는 전송에 영향을 줄 수 있는 필드에는 짝수 태그"
"가 사용된다. 따라서 인식할 수 없는 짝수 필드가 있는 인스크립션은 “unbound”, "
"즉 위치가 없는 상태로 표시되어야 한다."
#: /workspaces/ord_ko/docs/src/inscriptions.md:93
msgid ""
"Odd tags are used for fields which do not affect creation, initial "
"assignment, or transfer, such as additional metadata, and thus are safe to "
"ignore."
msgstr ""
"홀수 태그는 추가 메타데이터와 같이 생성, 초기 할당 또는 전송에 영향을 주지 않"
"는 필드에 사용되므로 무시해도 안전하다."
#: /workspaces/ord_ko/docs/src/inscriptions.md:96
#44,987,273text#: /workspaces/ord_ko/docs/src/guides/sat-hunting.md:239
msgid "Sparrow Wallet"
msgstr "스패로우 월렛"
#: /workspaces/ord_ko/docs/src/SUMMARY.md:18
#: /workspaces/ord_ko/docs/src/guides/testing.md:1
msgid "Testing"
msgstr "테스팅"
#: /workspaces/ord_ko/docs/src/SUMMARY.md:19
#: /workspaces/ord_ko/docs/src/guides/moderation.md:1
msgid "Moderation"
msgstr "중재"
#: /workspaces/ord_ko/docs/src/SUMMARY.md:20
#: /workspaces/ord_ko/docs/src/guides/reindexing.md:1
msgid "Reindexing"
msgstr "재인덱싱"
#: /workspaces/ord_ko/docs/src/SUMMARY.md:21
msgid "Bounties"
msgstr "현상금"
#: /workspaces/ord_ko/docs/src/SUMMARY.md:22
msgid "Bounty 0: 100,000 sats Claimed!"
msgstr "현상금 0: 100,000 SAT 획득!"
#: /workspaces/ord_ko/docs/src/SUMMARY.md:23
msgid "Bounty 1: 200,000 sats Claimed!"
msgstr "현상금 1: 200,000 SAT 획득!"
#: /workspaces/ord_ko/docs/src/SUMMARY.md:24
msgid "Bounty 2: 300,000 sats Claimed!"
msgstr "현상금 2: 300,000 SAT 획득!"
#: /workspaces/ord_ko/docs/src/SUMMARY.md:25
msgid "Bounty 3: 400,000 sats"
msgstr "현상금 3: 400,000 SAT"
#: /workspaces/ord_ko/docs/src/introduction.md:4
#44,987,268text"input or a combination of both. In any case the ordering is clear, since a "
"parser would go through the inputs consecutively and look for all "
"inscription `envelopes`."
msgstr ""
"인스크립션은 다른 입력 아니면 동일한 입력 내에 위치하거나 둘 다 일수 있다. 어"
"떤 경우든 구문 분석기는 입력을 연속적으로 살펴보고 모든 인스크립션 ‘엔벨"
"롭’을 찾기 때문에 순서는 명확하다."
#: /workspaces/ord_ko/docs/src/inscriptions.md:112
msgid "Input"
msgstr "입력"
#: /workspaces/ord_ko/docs/src/inscriptions.md:112
msgid "Inscription Count"
msgstr "인스크립션 개수"
#: /workspaces/ord_ko/docs/src/inscriptions.md:112
msgid "Indices"
msgstr "인덱스"
#: /workspaces/ord_ko/docs/src/inscriptions.md:114
#: /workspaces/ord_ko/docs/src/inscriptions.md:117
msgid "0"
msgstr "0"
#: /workspaces/ord_ko/docs/src/inscriptions.md:114
#: /workspaces/ord_ko/docs/src/inscriptions.md:116
msgid "2"
msgstr "2"
#: /workspaces/ord_ko/docs/src/inscriptions.md:114
msgid "i0, i1"
msgstr "i0, i1"
#: /workspaces/ord_ko/docs/src/inscriptions.md:115
#: /workspaces/ord_ko/docs/src/inscriptions.md:118
msgid "1"
msgstr "1"
#: /workspaces/ord_ko/docs/src/inscriptions.md:115
#44,987,254textmsgid "i2"
msgstr "i2"
#: /workspaces/ord_ko/docs/src/inscriptions.md:116
#: /workspaces/ord_ko/docs/src/inscriptions.md:117
msgid "3"
msgstr "3"
#: /workspaces/ord_ko/docs/src/inscriptions.md:116
msgid "i3, i4, i5"
msgstr "i3, i4, i5"
#: /workspaces/ord_ko/docs/src/inscriptions.md:118
msgid "4"
msgstr "4"
#: /workspaces/ord_ko/docs/src/inscriptions.md:118
msgid "i6"
msgstr "i6"
#: /workspaces/ord_ko/docs/src/inscriptions.md:120
msgid "Sandboxing"
msgstr "샌드박싱 (sandboxing)"
#: /workspaces/ord_ko/docs/src/inscriptions.md:123
msgid ""
"HTML and SVG inscriptions are sandboxed in order to prevent references to "
"off-chain content, thus keeping inscriptions immutable and self-contained."
msgstr ""
"오프체인 콘텐츠에 대한 참조를 방지하기 위해 HTML 및 SVG 인스크립션은 샌드박스"
"가 적용되어 인스크립션의 변경을 불가능하게 하고 독립적으로 유지될 수 있게 한"
"다."
#: /workspaces/ord_ko/docs/src/inscriptions.md:126
msgid ""
"This is accomplished by loading HTML and SVG inscriptions inside `iframes` "
"with the `sandbox` attribute, as well as serving inscription content with "
#44,987,253textmsgid "Metadata"
msgstr "메타 데이터"
#: /workspaces/ord_ko/docs/src/SUMMARY.md:7
#: /workspaces/ord_ko/docs/src/inscriptions/provenance.md:1
msgid "Provenance"
msgstr "기원"
#: /workspaces/ord_ko/docs/src/SUMMARY.md:8
#: /workspaces/ord_ko/docs/src/inscriptions/recursion.md:1
msgid "Recursion"
msgstr "리커젼"
#: /workspaces/ord_ko/docs/src/SUMMARY.md:9
msgid "FAQ"
msgstr "자주 묻는 질문"
#: /workspaces/ord_ko/docs/src/SUMMARY.md:10
msgid "Contributing"
msgstr "기여하는 방법"
#: /workspaces/ord_ko/docs/src/SUMMARY.md:11
#: /workspaces/ord_ko/docs/src/donate.md:1
msgid "Donate"
msgstr "기부하는 방법"
#: /workspaces/ord_ko/docs/src/SUMMARY.md:12
msgid "Guides"
msgstr "안내서들"
#: /workspaces/ord_ko/docs/src/SUMMARY.md:13
msgid "Explorer"
msgstr "탐색기"
#: /workspaces/ord_ko/docs/src/SUMMARY.md:15
#: /workspaces/ord_ko/docs/src/guides/sat-hunting.md:1
msgid "Sat Hunting"
msgstr "SAT 헌팅"
#: /workspaces/ord_ko/docs/src/SUMMARY.md:16
#: /workspaces/ord_ko/docs/src/guides/collecting.md:1
msgid "Collecting"
msgstr "수집"
#: /workspaces/ord_ko/docs/src/SUMMARY.md:17
#44,987,252text fn search_by_query_returns_rune() {
TestServer::new().assert_redirect("/search?query=ABCD", "/rune/ABCD");
}
#[test]
fn search_by_query_returns_inscription() {
TestServer::new().assert_redirect(
"/search?query=0000000000000000000000000000000000000000000000000000000000000000i0",
"/inscription/0000000000000000000000000000000000000000000000000000000000000000i0",
);
}
#[test]
fn search_is_whitespace_insensitive() {
TestServer::new().assert_redirect("/search/ 0 ", "/sat/0");
}
#[test]
fn search_by_path_returns_sat() {
TestServer::new().assert_redirect("/search/0", "/sat/0");
}
#[test]
fn search_for_blockhash_returns_block() {
TestServer::new().assert_redirect(
"/search/000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
"/block/000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
);
}
#[test]
fn search_for_txid_returns_transaction() {
TestServer::new().assert_redirect(
"/search/0000000000000000000000000000000000000000000000000000000000000000",
#44,986,647text if response.text().unwrap() == "2" {
break;
}
if i == 400 {
panic!("server failed to sync");
}
thread::sleep(Duration::from_millis(25));
}
child.kill().unwrap();
}
#[test]
fn missing_credentials() {
let rpc_server = test_bitcoincore_rpc::spawn();
CommandBuilder::new("--bitcoin-rpc-user foo server")
.rpc_server(&rpc_server)
.expected_exit_code(1)
.expected_stderr("error: no bitcoind rpc password specified\n")
.run_and_extract_stdout();
CommandBuilder::new("--bitcoin-rpc-pass bar server")
.rpc_server(&rpc_server)
.expected_exit_code(1)
.expected_stderr("error: no bitcoind rpc user specified\n")
.run_and_extract_stdout();
}
#[test]
fn all_endpoints_in_recursive_directory_return_json() {
let rpc_server = test_bitcoincore_rpc::spawn();
create_wallet(&rpc_server);
rpc_server.mine_blocks(2);
let server = TestServer::spawn_with_args(&rpc_server, &[]);
assert_eq!(server.request("/r/blockheight").json::<u64>().unwrap(), 2);
assert_eq!(server.request("/r/blocktime").json::<u64>().unwrap(), 2);
#44,985,504textuse super::*;
pub(crate) trait ToArgs {
fn to_args(&self) -> Vec<String>;
}
impl ToArgs for String {
fn to_args(&self) -> Vec<String> {
self.as_str().to_args()
}
}
impl ToArgs for &str {
fn to_args(&self) -> Vec<String> {
self.split_whitespace().map(str::to_string).collect()
}
}
impl<const N: usize> ToArgs for [&str; N] {
fn to_args(&self) -> Vec<String> {
self.iter().cloned().map(str::to_string).collect()
}
}
impl ToArgs for Vec<String> {
fn to_args(&self) -> Vec<String> {
self.clone()
}
}
pub(crate) struct CommandBuilder {
args: Vec<String>,
expected_exit_code: i32,
expected_stderr: Expected,
expected_stdout: Expected,
rpc_server_url: Option<String>,
stdin: Vec<u8>,
tempdir: TempDir,
}
impl CommandBuilder {
pub(crate) fn new(args: impl ToArgs) -> Self {
Self {
args: args.to_args(),
expected_exit_code: 0,
expected_stderr: Expected::String(String::new()),
expected_stdout: Expected::String(String::new()),
rpc_server_url: None,
#44,985,501text let port = TcpListener::bind("127.0.0.1:0")
.unwrap()
.local_addr()
.unwrap()
.port();
let mut child = Command::new(executable_path("ord"))
.args(format!(
"--rpc-url {} --bitcoin-data-dir {} --data-dir {} server --http-port {port} --address 127.0.0.1",
rpc_server.url(),
tempdir.path().display(),
tempdir.path().display()).to_args()
)
.env("ORD_BITCOIN_RPC_PASS", "bar")
.env("ORD_BITCOIN_RPC_USER", "foo")
.env("ORD_INTEGRATION_TEST", "1")
.current_dir(&tempdir)
.spawn().unwrap();
for i in 0.. {
match reqwest::blocking::get(format!("http://127.0.0.1:{port}/status")) {
Ok(_) => break,
Err(err) => {
if i == 400 {
panic!("Server failed to start: {err}");
}
}
}
thread::sleep(Duration::from_millis(25));
}
rpc_server.mine_blocks(1);
for i in 0.. {
let response = reqwest::blocking::get(format!("http://127.0.0.1:{port}/blockcount")).unwrap();
assert_eq!(response.status(), StatusCode::OK);
#44,985,500textuse {super::*, ord::subcommand::traits::Output, ord::Rarity};
#[test]
fn traits_command_prints_sat_traits() {
assert_eq!(
CommandBuilder::new("traits 0").run_and_deserialize_output::<Output>(),
Output {
number: 0,
decimal: "0.0".into(),
degree: "0°0′0″0‴".into(),
name: "nvtdijuwxlp".into(),
height: 0,
cycle: 0,
epoch: 0,
period: 0,
offset: 0,
rarity: Rarity::Mythic,
}
);
}
#[test]
fn traits_command_for_last_sat() {
assert_eq!(
CommandBuilder::new("traits 2099999997689999").run_and_deserialize_output::<Output>(),
Output {
number: 2099999997689999,
decimal: "6929999.0".into(),
degree: "5°209999′1007″0‴".into(),
name: "a".into(),
height: 6929999,
cycle: 5,
epoch: 32,
period: 3437,
offset: 0,
rarity: Rarity::Uncommon,
}
);
}
#44,985,499textuse {super::*, ord::subcommand::parse::Output, ord::Object};
#[test]
fn name() {
assert_eq!(
CommandBuilder::new("parse a").run_and_deserialize_output::<Output>(),
Output {
object: Object::Integer(2099999997689999),
}
);
}
#[test]
fn hash() {
assert_eq!(
CommandBuilder::new("parse 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
.run_and_deserialize_output::<Output>(),
Output {
object: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
.parse::<Object>()
.unwrap(),
}
);
}
#[test]
fn unrecognized_object() {
CommandBuilder::new("parse A")
.stderr_regex(r"error: .*: unrecognized object\n.*")
.expected_exit_code(2)
.run_and_extract_stdout();
}
#44,985,498textuse {
super::*, crate::command_builder::ToArgs, ciborium::value::Integer,
ord::subcommand::wallet::send::Output,
};
#[test]
fn run() {
let rpc_server = test_bitcoincore_rpc::spawn();
let port = TcpListener::bind("127.0.0.1:0")
.unwrap()
.local_addr()
.unwrap()
.port();
let builder = CommandBuilder::new(format!("server --address 127.0.0.1 --http-port {port}"))
.rpc_server(&rpc_server);
let mut command = builder.command();
let mut child = command.spawn().unwrap();
for attempt in 0.. {
if let Ok(response) = reqwest::blocking::get(format!("http://localhost:{port}/status")) {
if response.status() == 200 {
assert_eq!(response.text().unwrap(), "OK");
break;
}
}
if attempt == 100 {
panic!("Server did not respond to status check",);
}
thread::sleep(Duration::from_millis(50));
}
child.kill().unwrap();
}
#[test]
fn inscription_page() {
let rpc_server = test_bitcoincore_rpc::spawn();
create_wallet(&rpc_server);
let (inscription, reveal) = inscribe(&rpc_server);
#44,985,495text rarity: Rarity::Common,
percentile: "0.000714285715119048%".into(),
satpoint: Some(SatPoint::from_str(&format!("{}:{}:{}", reveal, 0, 0)).unwrap()),
timestamp: 3,
inscriptions: vec![inscription_id],
}
)
}
#[test]
fn get_inscription() {
let rpc_server = test_bitcoincore_rpc::spawn();
create_wallet(&rpc_server);
let (inscription_id, reveal) = inscribe(&rpc_server);
let response =
TestServer::spawn_with_server_args(&rpc_server, &["--index-sats"], &["--enable-json-api"])
.json_request(format!("/inscription/{}", inscription_id));
assert_eq!(response.status(), StatusCode::OK);
let mut inscription_json: InscriptionJson =
serde_json::from_str(&response.text().unwrap()).unwrap();
assert_regex_match!(inscription_json.address.unwrap(), r"bc1p.*");
inscription_json.address = None;
pretty_assert_eq!(
inscription_json,
InscriptionJson {
parent: None,
children: Vec::new(),
inscription_id,
inscription_number: 0,
genesis_height: 2,
#44,985,494textuse {super::*, ord::subcommand::subsidy::Output};
#[test]
fn genesis() {
assert_eq!(
CommandBuilder::new("subsidy 0").run_and_deserialize_output::<Output>(),
Output {
first: 0,
subsidy: 5000000000,
name: "nvtdijuwxlp".into(),
}
);
}
#[test]
fn second_block() {
assert_eq!(
CommandBuilder::new("subsidy 1").run_and_deserialize_output::<Output>(),
Output {
first: 5000000000,
subsidy: 5000000000,
name: "nvtcsezkbth".into(),
}
);
}
#[test]
fn second_to_last_block_with_subsidy() {
assert_eq!(
CommandBuilder::new("subsidy 6929998").run_and_deserialize_output::<Output>(),
Output {
first: 2099999997689998,
subsidy: 1,
name: "b".into(),
}
);
}
#[test]
fn last_block_with_subsidy() {
assert_eq!(
CommandBuilder::new("subsidy 6929999").run_and_deserialize_output::<Output>(),
Output {
first: 2099999997689999,
subsidy: 1,
name: "a".into(),
}
);
}
#[test]
fn first_block_without_subsidy() {
#44,985,492text#![allow(clippy::type_complexity)]
use {
self::{command_builder::CommandBuilder, expected::Expected, test_server::TestServer},
bitcoin::{
address::{Address, NetworkUnchecked},
blockdata::constants::COIN_VALUE,
Network, OutPoint, Txid,
},
executable_path::executable_path,
ord::{
inscription_id::InscriptionId,
rarity::Rarity,
templates::{
block::BlockJson, inscription::InscriptionJson, inscriptions::InscriptionsJson,
output::OutputJson, sat::SatJson,
},
SatPoint,
},
pretty_assertions::assert_eq as pretty_assert_eq,
regex::Regex,
reqwest::{StatusCode, Url},
serde::de::DeserializeOwned,
std::{
collections::BTreeMap,
fs,
io::Write,
net::TcpListener,
path::Path,
process::{Child, Command, Stdio},
str::{self, FromStr},
thread,
time::Duration,
},
tempfile::TempDir,
test_bitcoincore_rpc::{Sent, TransactionTemplate},
};
macro_rules! assert_regex_match {
($string:expr, $pattern:expr $(,)?) => {
let regex = Regex::new(&format!("^(?s){}$", $pattern)).unwrap();
#44,985,486text ord_server.assert_response_regex(
format!("/inscription/{inscription}"),
format!(
r".*<h1>Inscription 0</h1>.*<dt>address</dt>\s*<dd class=monospace>bc1qcqgs2pps4u4yedfyl5pysdjjncs8et5utseepv</dd>.*<dt>location</dt>\s*<dd class=monospace>{txid}:0:0</dd>.*",
),
)
}
#[test]
fn inscription_content() {
let rpc_server = test_bitcoincore_rpc::spawn();
create_wallet(&rpc_server);
rpc_server.mine_blocks(1);
let (inscription, _) = inscribe(&rpc_server);
rpc_server.mine_blocks(1);
let response =
TestServer::spawn_with_args(&rpc_server, &[]).request(format!("/content/{inscription}"));
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response.headers().get("content-type").unwrap(),
"text/plain;charset=utf-8"
);
assert_eq!(
response
.headers()
.get_all("content-security-policy")
.into_iter()
.collect::<Vec<&http::HeaderValue>>(),
&[
"default-src 'self' 'unsafe-eval' 'unsafe-inline' data: blob:",
"default-src *:*/content/ *:*/blockheight *:*/blockhash *:*/blockhash/ *:*/blocktime *:*/r/ 'unsafe-eval' 'unsafe-inline' data: blob:",
#44,985,481textuse {super::*, ord::subcommand::wallet::create::Output};
#[test]
fn create() {
let rpc_server = test_bitcoincore_rpc::spawn();
assert!(!rpc_server.wallets().contains("ord"));
CommandBuilder::new("wallet create")
.rpc_server(&rpc_server)
.run_and_deserialize_output::<Output>();
assert!(rpc_server.wallets().contains("ord"));
}
#[test]
fn seed_phrases_are_twelve_words_long() {
let Output { mnemonic, .. } = CommandBuilder::new("wallet create")
.rpc_server(&test_bitcoincore_rpc::spawn())
.run_and_deserialize_output();
assert_eq!(mnemonic.word_count(), 12);
}
#[test]
fn wallet_creates_correct_mainnet_taproot_descriptor() {
let rpc_server = test_bitcoincore_rpc::spawn();
CommandBuilder::new("wallet create")
.rpc_server(&rpc_server)
.run_and_deserialize_output::<Output>();
assert_eq!(rpc_server.descriptors().len(), 2);
assert_regex_match!(
&rpc_server.descriptors()[0],
r"tr\(\[[[:xdigit:]]{8}/86'/0'/0'\]xprv[[:alnum:]]*/0/\*\)#[[:alnum:]]{8}"
);
assert_regex_match!(
#44,985,474text ) -> ServerResult<Json<SatInscriptionJson>> {
if !index.has_sat_index() {
return Err(ServerError::NotFound(
"this server has no sat index".to_string(),
));
}
let id = index.get_inscription_id_by_sat_indexed(sat, inscription_index)?;
Ok(Json(SatInscriptionJson { id }))
}
async fn redirect_http_to_https(
Extension(mut destination): Extension<String>,
uri: Uri,
) -> Redirect {
if let Some(path_and_query) = uri.path_and_query() {
destination.push_str(path_and_query.as_str());
}
Redirect::to(&destination)
}
}
#[cfg(test)]
mod tests {
use {
super::*,
crate::runes::{Edict, Etching, Rune, Runestone},
reqwest::Url,
serde::de::DeserializeOwned,
std::net::TcpListener,
};
const RUNE: u128 = 99246114928149462;
struct TestServer {
bitcoin_rpc_server: test_bitcoincore_rpc::Handle,
index: Arc<Index>,
ord_server_handle: Handle,
url: Url,
#[allow(unused)]
tempdir: TempDir,
}
impl TestServer {
#44,985,420text rune,
supply: u128::max_value(),
timestamp: 2,
..Default::default()
}
)]
);
let output = OutPoint { txid, vout: 0 };
assert_eq!(
server.index.get_rune_balances(),
[(output, vec![(id, u128::max_value())])]
);
server.assert_response_regex(
format!("/output/{output}"),
StatusCode::OK,
format!(
".*<title>Output {output}</title>.*<h1>Output <span class=monospace>{output}</span></h1>.*
<dt>runes</dt>
<dd>
<table>
<tr>
<th>rune</th>
<th>balance</th>
</tr>
<tr>
<td><a href=/rune/AAAAAAAAAAAAA>AAAAAAAAAAAAA</a></td>
<td>34028236692093846346337460743176821145.5</td>
</tr>
</table>
</dd>
.*"
),
);
assert_eq!(
server.get_json::<OutputJson>(format!("/output/{output}")),
OutputJson {
value: 5000000000,
script_pubkey: String::new(),
address: None,
transaction: txid.to_string(),
sat_ranges: None,
#44,985,419text
let index_thread = thread::spawn(move || loop {
if SHUTTING_DOWN.load(atomic::Ordering::Relaxed) {
break;
}
if let Err(error) = index_clone.update() {
log::warn!("Updating index: {error}");
}
thread::sleep(Duration::from_millis(5000));
});
INDEXER.lock().unwrap().replace(index_thread);
let server_config = Arc::new(ServerConfig {
is_json_api_enabled: self.enable_json_api,
});
let config = options.load_config()?;
let acme_domains = self.acme_domains()?;
let page_config = Arc::new(PageConfig {
chain: options.chain(),
csp_origin: self.csp_origin.clone(),
domain: acme_domains.first().cloned(),
index_sats: index.has_sat_index(),
});
let router = Router::new()
.route("/", get(Self::home))
.route("/block/:query", get(Self::block))
.route("/blockcount", get(Self::block_count))
.route("/blockhash", get(Self::block_hash))
.route("/blockhash/:height", get(Self::block_hash_from_height))
#44,985,418text );
assert_eq!(
server.index.get_rune_balances(),
[(OutPoint { txid, vout: 0 }, vec![(id, u128::max_value())])]
);
server.assert_response_regex(
"/runes",
StatusCode::OK,
".*<title>Runes</title>.*
<h1>Runes</h1>
<ul>
<li><a href=/rune/AAAAAAAAAAAAA>AAAAAAAAAAAAA</a></li>
</ul>.*",
);
}
#[test]
fn runes_are_displayed_on_rune_page() {
let server = TestServer::new_with_regtest_with_index_runes();
server.mine_blocks(1);
let rune = Rune(RUNE);
server.assert_response_regex(format!("/rune/{rune}"), StatusCode::NOT_FOUND, ".*");
let txid = server.bitcoin_rpc_server.broadcast_tx(TransactionTemplate {
inputs: &[(1, 0, 0, inscription("text/plain", "hello").to_witness())],
op_return: Some(
Runestone {
edicts: vec![Edict {
id: 0,
amount: u128::max_value(),
output: 0,
}],
etching: Some(Etching {
rune,
symbol: Some('$'),
..Default::default()
#44,985,417text inscriptions: Vec::new(),
runes: vec![(Rune(RUNE), 340282366920938463463374607431768211455)]
.into_iter()
.collect(),
}
);
}
#[test]
fn http_to_https_redirect_with_path() {
TestServer::new_with_args(&[], &["--redirect-http-to-https", "--https"]).assert_redirect(
"/sat/0",
&format!("https://{}/sat/0", System::new().host_name().unwrap()),
);
}
#[test]
fn http_to_https_redirect_with_empty() {
TestServer::new_with_args(&[], &["--redirect-http-to-https", "--https"]).assert_redirect(
"/",
&format!("https://{}/", System::new().host_name().unwrap()),
);
}
#[test]
fn status() {
TestServer::new().assert_response("/status", StatusCode::OK, "OK");
}
#[test]
fn block_count_endpoint() {
let test_server = TestServer::new();
let response = test_server.get("/blockcount");
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(response.text().unwrap(), "1");
test_server.mine_blocks(1);
#44,985,416text );
}
#[test]
fn empty_range_returns_400() {
TestServer::new().assert_response("/range/0/0", StatusCode::BAD_REQUEST, "empty range");
}
#[test]
fn range() {
TestServer::new().assert_response_regex(
"/range/0/1",
StatusCode::OK,
r".*<title>Sat range 0–1</title>.*<h1>Sat range 0–1</h1>
<dl>
<dt>value</dt><dd>1</dd>
<dt>first</dt><dd><a href=/sat/0 class=mythic>0</a></dd>
</dl>.*",
);
}
#[test]
fn sat_number() {
TestServer::new().assert_response_regex("/sat/0", StatusCode::OK, ".*<h1>Sat 0</h1>.*");
}
#[test]
fn sat_decimal() {
TestServer::new().assert_response_regex("/sat/0.0", StatusCode::OK, ".*<h1>Sat 0</h1>.*");
}
#[test]
fn sat_degree() {
TestServer::new().assert_response_regex("/sat/0°0′0″0‴", StatusCode::OK, ".*<h1>Sat 0</h1>.*");
}
#[test]
fn sat_name() {
TestServer::new().assert_response_regex(
"/sat/nvtdijuwxlp",
StatusCode::OK,
".*<h1>Sat 0</h1>.*",
);
}
#[test]
fn sat() {
TestServer::new().assert_response_regex(
#44,985,415text